diff --git a/app/build.gradle b/app/build.gradle index 1ba26aeef..9166a16a9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -305,7 +305,6 @@ dependencies { implementation 'com.github.mario:PopupBubble:a365177d96' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'eu.medsea.mimeutil:mime-util:2.1.3' - implementation 'com.github.izjumovfs:SwipeToReply:1.0.1' implementation 'com.afollestad.material-dialogs:core:3.1.0' implementation 'com.afollestad.material-dialogs:datetime:3.1.0' diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt b/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt index 589427492..0428f89a9 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt @@ -47,6 +47,7 @@ import com.nextcloud.talk.R import com.nextcloud.talk.activities.MainActivity import com.nextcloud.talk.controllers.SwitchAccountController import com.nextcloud.talk.controllers.base.providers.ActionBarProvider +import com.nextcloud.talk.newarch.utils.dp import com.nextcloud.talk.newarch.utils.px import com.nextcloud.talk.utils.preferences.AppPreferences import com.uber.autodispose.lifecycle.LifecycleScopeProvider @@ -183,7 +184,7 @@ abstract class BaseController : ButterKnifeController(), ComponentCallbacks { if (getAppBarLayoutType() != AppBarLayoutType.EMPTY) { it.toolbar.isVisible = !value - appBarLayoutParams.height = 56.px + appBarLayoutParams.height = 56.dp } else { appBarLayoutParams.height = 0 } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt index 976e3a823..fedb48ac5 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt @@ -54,7 +54,6 @@ import com.bluelinelabs.conductor.archlifecycle.ControllerLifecycleOwner import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler -import com.capybaralabs.swipetoreply.ISwipeControllerActions import com.nextcloud.talk.R import com.nextcloud.talk.activities.MagicCallActivity import com.nextcloud.talk.callbacks.MentionAutocompleteCallback @@ -69,9 +68,10 @@ import com.nextcloud.talk.newarch.local.models.getMaxMessageLength import com.nextcloud.talk.newarch.local.models.toUserEntity import com.nextcloud.talk.newarch.mvvm.BaseView import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView -import com.nextcloud.talk.newarch.utils.ChatSwipeCallback +import com.nextcloud.talk.newarch.utils.swipe.ChatMessageSwipeCallback import com.nextcloud.talk.newarch.utils.Images import com.nextcloud.talk.newarch.utils.NetworkComponents +import com.nextcloud.talk.newarch.utils.swipe.ChatMessageSwipeInterface import com.nextcloud.talk.presenters.MentionAutocompletePresenter import com.nextcloud.talk.utils.* import com.nextcloud.talk.utils.AccountUtils.canWeOpenFilesApp @@ -249,13 +249,16 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface { view.cancelReplyButton.setOnClickListener { hideReplyView() } - val controller = ChatSwipeCallback(messagesAdapter, context, ISwipeControllerActions { - val element = messagesAdapter.elementAt(it) - if (element != null) { - val adapterChatElement = element.element as Element - if (adapterChatElement.data is ChatElement) { - val chatElement = adapterChatElement.data as ChatElement - view.messagesRecyclerView.postDelayed({ showReplyView(chatElement.data as ChatMessage)}, 125) + val controller = ChatMessageSwipeCallback(context, messagesAdapter, object : ChatMessageSwipeInterface { + override fun onSwipePerformed(position: Int) { + val element = messagesAdapter.elementAt(position) + if (element != null) { + val adapterChatElement = element.element as Element + if (adapterChatElement.data is ChatElement) { + val chatElement = adapterChatElement.data as ChatElement + showReplyView(chatElement.data as ChatMessage) + //view.messagesRecyclerView.postDelayed({ showReplyView(chatElement.data as ChatMessage)}, 125) + } } } }) diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsView.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsView.kt index 6ce37aeab..393deab30 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsView.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsView.kt @@ -49,6 +49,7 @@ import com.nextcloud.talk.newarch.local.models.toUser import com.nextcloud.talk.newarch.mvvm.BaseView import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView import com.nextcloud.talk.newarch.utils.ElementPayload +import com.nextcloud.talk.newarch.utils.dp import com.nextcloud.talk.newarch.utils.px import com.nextcloud.talk.utils.bundle.BundleKeys import com.otaliastudios.elements.Adapter @@ -98,8 +99,8 @@ class ContactsView(private val bundle: Bundle? = null) : BaseView() { .addPresenter(Presenter.forLoadingIndicator(activity as Context, R.layout.loading_state)) .addPresenter(AdvancedEmptyPresenter(activity as Context, R.layout.message_state, null) { view -> val layoutParams = view.messageStateImageView.layoutParams as RelativeLayout.LayoutParams - layoutParams.height = 128.px - layoutParams.width = 128.px + layoutParams.height = 128.dp + layoutParams.width = 128.dp view.messageStateImageView.layoutParams = layoutParams view.messageStateTextView.setText(R.string.nc_search_empty_contacts) view.messageStateImageView.load(context.getDrawable(R.drawable.ic_undraw_not_found_60pq)) @@ -107,8 +108,8 @@ class ContactsView(private val bundle: Bundle? = null) : BaseView() { }) .addPresenter(Presenter.forErrorIndicator(activity as Context, R.layout.message_state) { view, throwable -> val layoutParams = view.messageStateImageView.layoutParams as RelativeLayout.LayoutParams - layoutParams.height = 128.px - layoutParams.width = 128.px + layoutParams.height = 128.dp + layoutParams.width = 128.dp view.messageStateImageView.layoutParams = layoutParams view.messageStateTextView.setText(R.string.nc_oops) view.messageStateImageView.load((activity as Context).getDrawable(R.drawable.ic_undraw_server_down_s4lk)) diff --git a/app/src/main/java/com/nextcloud/talk/newarch/utils/ChatSwipeCallback.kt b/app/src/main/java/com/nextcloud/talk/newarch/utils/ChatSwipeCallback.kt deleted file mode 100644 index 719fbbf04..000000000 --- a/app/src/main/java/com/nextcloud/talk/newarch/utils/ChatSwipeCallback.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.nextcloud.talk.newarch.utils - -import android.content.Context -import androidx.recyclerview.widget.RecyclerView -import com.capybaralabs.swipetoreply.ISwipeControllerActions -import com.capybaralabs.swipetoreply.SwipeController -import com.nextcloud.talk.newarch.features.chat.ChatElement -import com.nextcloud.talk.newarch.features.chat.ChatElementTypes -import com.otaliastudios.elements.Adapter -import com.otaliastudios.elements.Element - -class ChatSwipeCallback(private val adapter: Adapter, context: Context?, swipeControllerActions: ISwipeControllerActions?) : SwipeController(context, swipeControllerActions) { - override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { - val position: Int = viewHolder.adapterPosition - val element = adapter.elementAt(position) - if (element != null) { - val adapterChatElement = element.element as Element - if (adapterChatElement.data is ChatElement) { - val chatElement = adapterChatElement.data as ChatElement - if (chatElement.elementType == ChatElementTypes.CHAT_MESSAGE) { - return super.getMovementFlags(recyclerView, viewHolder) - } - } - } - return 0 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/utils/Extensions.kt b/app/src/main/java/com/nextcloud/talk/newarch/utils/Extensions.kt index caf1abfa2..5a15ddc49 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/utils/Extensions.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/utils/Extensions.kt @@ -34,8 +34,8 @@ fun String.hashWithAlgorithm(algorithm: String): String { return bytes.fold("", { str, it -> str + "%02x".format(it) }) } -val Int.dp: Int +val Int.px: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt() -val Int.px: Int +val Int.dp: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt() \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/utils/swipe/ChatMessageSwipeCallback.kt b/app/src/main/java/com/nextcloud/talk/newarch/utils/swipe/ChatMessageSwipeCallback.kt new file mode 100644 index 000000000..1109bba52 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/utils/swipe/ChatMessageSwipeCallback.kt @@ -0,0 +1,230 @@ +/* + * + * * Nextcloud Talk application + * * + * * @author Mario Danic + * * Copyright (C) 2017-2020 Mario Danic + * * + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * at your option) any later version. + * * + * * This program is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU General Public License for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see . + * + * Inspired by: https://github.com/izjumovfs/SwipeToReply + */ +package com.nextcloud.talk.newarch.utils.swipe + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.PorterDuff +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.HapticFeedbackConstants +import android.view.MotionEvent +import android.view.View +import android.view.View.OnTouchListener +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.newarch.features.chat.ChatElement +import com.nextcloud.talk.newarch.features.chat.ChatElementTypes +import com.nextcloud.talk.newarch.utils.dp +import com.nextcloud.talk.newarch.utils.px +import com.otaliastudios.elements.Adapter +import com.otaliastudios.elements.Element +import kotlin.math.abs +import kotlin.math.min + +open class ChatMessageSwipeCallback : ItemTouchHelper.Callback { + private var context: Context + private var adapter: Adapter + private var swiped: Boolean = false + private var chatMessageSwipeInterface: ChatMessageSwipeInterface + private var replyIcon: Drawable + private var replyIconBackground: Drawable + private var currentViewHolder: RecyclerView.ViewHolder? = null + private var view: View? = null + private var dx = 0f + private var replyButtonProgress = 0f + private var lastReplyButtonAnimationTime: Long = 0 + private var swipeBack = false + private var isVibrating = false + private var startTracking = false + private var mBackgroundColor = 0x20606060 + private val replyBackgroundOffset = 18 + private val replyIconXOffset = 12 + private val replyIconYOffset = 11 + + constructor(context: Context, adapter: Adapter, chatMessageSwipeInterface: ChatMessageSwipeInterface) { + this.context = context + this.adapter = adapter + this.chatMessageSwipeInterface = chatMessageSwipeInterface + replyIcon = context.resources.getDrawable(R.drawable.ic_reply_white_24dp) + replyIconBackground = context.resources.getDrawable(R.drawable.ic_round_shape) + } + + constructor(context: Context, adapter: Adapter, chatMessageSwipeInterface: ChatMessageSwipeInterface, replyIcon: Int, replyIconBackground: Int, backgroundColor: Int) { + this.context = context + this.adapter = adapter + this.chatMessageSwipeInterface = chatMessageSwipeInterface + this.replyIcon = context.resources.getDrawable(replyIcon) + this.replyIconBackground = context.resources.getDrawable(replyIconBackground) + mBackgroundColor = backgroundColor + } + + + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + val position: Int = viewHolder.adapterPosition + val element = adapter.elementAt(position) + if (element != null) { + val adapterChatElement = element.element as Element + if (adapterChatElement.data is ChatElement) { + val chatElement = adapterChatElement.data as ChatElement + if (chatElement.elementType == ChatElementTypes.CHAT_MESSAGE) { + view = viewHolder.itemView + return makeMovementFlags(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.RIGHT) + } + } + } + return 0 + } + + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + return false + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + } + + override fun convertToAbsoluteDirection(flags: Int, layoutDirection: Int): Int { + if (swipeBack) { + swipeBack = false + return 0 + } + return super.convertToAbsoluteDirection(flags, layoutDirection) + } + + override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + setTouchListener(recyclerView, viewHolder) + } + if (view!!.translationX < convertToDp(130) || dX < dx) { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + dx = dX + startTracking = true + } + currentViewHolder = viewHolder + drawReplyButton(c) + } + + @SuppressLint("ClickableViewAccessibility") + private fun setTouchListener(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + recyclerView.setOnTouchListener(OnTouchListener { v, event -> + swipeBack = event.action == MotionEvent.ACTION_CANCEL || event.action == MotionEvent.ACTION_UP + if (swipeBack) { + if (abs(view!!.translationX) >= convertToDp(100)) { + swiped = true + } + } + false + }) + } + + private fun convertToDp(pixels: Int): Int { + return pixels.dp + } + + private fun drawReplyButton(canvas: Canvas) { + currentViewHolder?.let { currentViewHolder -> + view?.let { view -> + val translationX = view.translationX + val newTime = System.currentTimeMillis() + val dt = min(17, newTime - lastReplyButtonAnimationTime) + lastReplyButtonAnimationTime = newTime + var showing = false + if (translationX >= convertToDp(30)) { + showing = true + } + if (showing) { + if (replyButtonProgress < 1.0f) { + replyButtonProgress += dt / 180.0f + if (replyButtonProgress > 1.0f) { + replyButtonProgress = 1.0f + } else { + view.invalidate() + } + } + } else if (translationX <= 0.0f) { + replyButtonProgress = 0f + startTracking = false + isVibrating = false + if (swiped) { + chatMessageSwipeInterface.onSwipePerformed(currentViewHolder.adapterPosition) + swiped = false + } + } else { + if (replyButtonProgress > 0.0f) { + replyButtonProgress -= dt / 180.0f + if (replyButtonProgress < 0.1f) { + replyButtonProgress = 0f + } + } + view.invalidate() + } + val alpha: Int + val scale: Float + if (showing) { + scale = if (replyButtonProgress <= 0.8f) { + 1.2f * (replyButtonProgress / 0.8f) + } else { + 1.2f - 0.2f * ((replyButtonProgress - 0.8f) / 0.2f) + } + alpha = min(255, 255 * (replyButtonProgress / 0.8f).toInt()) + } else { + scale = replyButtonProgress + alpha = min(255, 255 * replyButtonProgress.toInt()) + } + replyIconBackground.alpha = alpha + replyIcon.alpha = alpha + if (startTracking) { + if (!isVibrating && view.translationX >= convertToDp(100)) { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) + } + isVibrating = true + } + val x: Int = if (view.translationX > convertToDp(130)) { + convertToDp(130) / 2 + } else { + view.translationX.toInt() / 2 + } + val y: Float = view.top + view.measuredHeight.toFloat() / 2 + replyIconBackground.setColorFilter(mBackgroundColor, PorterDuff.Mode.MULTIPLY) + replyIconBackground.bounds = Rect( + (x - convertToDp(replyBackgroundOffset) * scale).toInt(), + (y - convertToDp(replyBackgroundOffset) * scale).toInt(), + (x + convertToDp(replyBackgroundOffset) * scale).toInt(), + (y + convertToDp(replyBackgroundOffset) * scale).toInt() + ) + replyIconBackground.draw(canvas) + replyIcon.bounds = Rect( + (x - convertToDp(replyIconXOffset) * scale).toInt(), + (y - convertToDp(replyIconYOffset) * scale).toInt(), + (x + convertToDp(replyIconXOffset) * scale).toInt(), + (y + convertToDp(replyIconYOffset) * scale).toInt() + ) + replyIcon.draw(canvas) + replyIconBackground.alpha = 255 + replyIcon.alpha = 255 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/utils/swipe/ChatMessageSwipeInterface.kt b/app/src/main/java/com/nextcloud/talk/newarch/utils/swipe/ChatMessageSwipeInterface.kt new file mode 100644 index 000000000..86210750f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/utils/swipe/ChatMessageSwipeInterface.kt @@ -0,0 +1,27 @@ +/* + * + * * Nextcloud Talk application + * * + * * @author Mario Danic + * * Copyright (C) 2017-2020 Mario Danic + * * + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * at your option) any later version. + * * + * * This program is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU General Public License for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see . + * + * Inspired by: https://github.com/izjumovfs/SwipeToReply + */ +package com.nextcloud.talk.newarch.utils.swipe + +interface ChatMessageSwipeInterface { + fun onSwipePerformed(position: Int) +} \ No newline at end of file diff --git a/app/src/main/res/layout/view_message_input.xml b/app/src/main/res/layout/view_message_input.xml index a5be3aba1..127f9526b 100644 --- a/app/src/main/res/layout/view_message_input.xml +++ b/app/src/main/res/layout/view_message_input.xml @@ -22,7 +22,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - +