From 2f01ad99b375a8d2f1011d6ece6e3c7ebf4a007d Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 16 Jul 2019 16:35:36 +0200 Subject: [PATCH 1/4] Compact long tap menu --- .../adapter_item_action_quick_reaction.xml | 18 ++---------------- .../layout/bottom_sheet_message_actions.xml | 1 + vector/src/main/res/values/strings.xml | 1 - 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml b/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml index 9f26e95f..16a53cff 100644 --- a/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml +++ b/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml @@ -7,18 +7,6 @@ android:layout_height="wrap_content" android:padding="8dp"> - - - + app:layout_constraintTop_toTopOf="parent" /> diff --git a/vector/src/main/res/layout/bottom_sheet_message_actions.xml b/vector/src/main/res/layout/bottom_sheet_message_actions.xml index d1cb8c9f..9fadcee1 100644 --- a/vector/src/main/res/layout/bottom_sheet_message_actions.xml +++ b/vector/src/main/res/layout/bottom_sheet_message_actions.xml @@ -28,6 +28,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0" tools:src="@tools:sample/avatars" /> Other third party notices You are already viewing this room! - Quick Reactions General From 9bdea5b325aa349f5128b14eb523f5ddedc0e233 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 16 Jul 2019 16:35:57 +0200 Subject: [PATCH 2/4] Change order of actions (and reply on top) --- .../timeline/action/MessageMenuViewModel.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index b49731d9..bc21de4d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -127,13 +127,6 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M //TODO is downloading attachement? if (!event.root.isRedacted()) { - if (event.canReact()) { - this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, eventId)) - } - if (canCopy(type)) { - //TODO copy images? html? see ClipBoard - this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent!!.body)) - } if (canReply(event, messageContent)) { this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId)) @@ -147,6 +140,15 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, eventId)) } + if (canCopy(type)) { + //TODO copy images? html? see ClipBoard + this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent!!.body)) + } + + if (event.canReact()) { + this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, eventId)) + } + if (canQuote(event, messageContent)) { this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId)) } From 9494174c3389edca915af23a2068cb1afa7002f4 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 17 Jul 2019 10:51:09 +0200 Subject: [PATCH 3/4] Swipe to reply in timeline (lab) --- CHANGES.md | 2 + .../home/room/detail/RoomDetailFragment.kt | 31 ++- .../detail/RoomMessageTouchHelperCallback.kt | 206 ++++++++++++++++++ .../features/settings/VectorPreferences.kt | 5 + vector/src/main/res/values/strings_riotX.xml | 1 + .../src/main/res/xml/vector_settings_labs.xml | 68 +++--- 6 files changed, 280 insertions(+), 33 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt diff --git a/CHANGES.md b/CHANGES.md index ee6d2c8c..f4c72ba7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,8 @@ Features: Improvements: - Handle click on redacted events: view source and create permalink + - Improve long tap menu: reply on top, more compact (#368) + - Quick reply in timeline with swipe gesture Other changes: - diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 943b6165..4f91539c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -37,9 +37,11 @@ import androidx.annotation.DrawableRes import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import butterknife.BindView +import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel @@ -57,6 +59,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.user.model.User @@ -87,7 +90,7 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.action.* import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.invite.VectorInviteView @@ -323,6 +326,32 @@ class RoomDetailFragment : }) recyclerView.setController(timelineEventController) timelineEventController.callback = this + + if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) { + val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + } + } + + override fun canSwipeModelModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) + val touchHelper = ItemTouchHelper(swipeCallback) + touchHelper.attachToRecyclerView(recyclerView) + } } private fun setupComposer() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt new file mode 100644 index 00000000..d716d6b4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt @@ -0,0 +1,206 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.home.room.detail + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.TypedValue +import android.view.HapticFeedbackConstants +import android.view.MotionEvent +import android.view.View +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_SWIPE +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.EpoxyTouchHelperCallback +import com.airbnb.epoxy.EpoxyViewHolder +import timber.log.Timber + + +class RoomMessageTouchHelperCallback(private val context: Context, + @DrawableRes actionIcon: Int, + private val handler: QuickReplayHandler) : EpoxyTouchHelperCallback() { + + interface QuickReplayHandler { + fun performQuickReplyOnHolder(model: EpoxyModel<*>) + fun canSwipeModelModel(model: EpoxyModel<*>): Boolean + } + + private var swipeBack: Boolean = false + private var dX = 0f + private var startTracking = false + private var isVibrate = false + + private var replyButtonProgress: Float = 0F + private var lastReplyButtonAnimationTime: Long = 0 + + private var imageDrawable: Drawable = ContextCompat.getDrawable(context, actionIcon)!! + + + private val triggerDistance = convertToPx(100) + private val minShowDistance = convertToPx(20) + private val triggerDelta = convertToPx(20) + + override fun onSwiped(viewHolder: EpoxyViewHolder?, direction: Int) { + + } + + override fun onMove(recyclerView: RecyclerView?, viewHolder: EpoxyViewHolder?, target: EpoxyViewHolder?): Boolean { + return false + } + + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: EpoxyViewHolder): Int { + if (handler.canSwipeModelModel(viewHolder.model)) { + return ItemTouchHelper.Callback.makeMovementFlags(0, ItemTouchHelper.START) //Should we use Left? + } else { + return 0 + } + } + + + //We never let items completely go out + 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: EpoxyViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { + if (actionState == ACTION_STATE_SWIPE) { + setTouchListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + } + val size = triggerDistance + if (Math.abs(viewHolder.itemView.translationX) < size || dX > this.dX /*going back*/) { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + this.dX = dX + startTracking = true + } + drawReplyButton(c, viewHolder.itemView) + } + + + @SuppressLint("ClickableViewAccessibility") + private fun setTouchListener(c: Canvas, + recyclerView: RecyclerView, + viewHolder: EpoxyViewHolder, + dX: Float, dY: Float, + actionState: Int, isCurrentlyActive: Boolean) { + //TODO can this interfer with other interactions? should i remove it + recyclerView.setOnTouchListener { v, event -> + swipeBack = event.action == MotionEvent.ACTION_CANCEL || event.action == MotionEvent.ACTION_UP + if (swipeBack) { + if (Math.abs(dX) >= triggerDistance) { + try { + viewHolder.model?.let { handler.performQuickReplyOnHolder(it) } + } catch (e: IllegalStateException) { + Timber.e(e) + } + } + } + false + } + } + + + private fun drawReplyButton(canvas: Canvas, itemView: View) { + + Timber.v("drawReplyButton") + val translationX = Math.abs(itemView.translationX) + val newTime = System.currentTimeMillis() + val dt = Math.min(17, newTime - lastReplyButtonAnimationTime) + lastReplyButtonAnimationTime = newTime + val showing = translationX >= minShowDistance + if (showing) { + if (replyButtonProgress < 1.0f) { + replyButtonProgress += dt / 180.0f + if (replyButtonProgress > 1.0f) { + replyButtonProgress = 1.0f + } else { + itemView.invalidate() + } + } + } else if (translationX <= 0.0f) { + replyButtonProgress = 0f + startTracking = false + isVibrate = false + } else { + if (replyButtonProgress > 0.0f) { + replyButtonProgress -= dt / 180.0f + if (replyButtonProgress < 0.1f) { + replyButtonProgress = 0f + } else { + itemView.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 = Math.min(255f, 255 * (replyButtonProgress / 0.8f)).toInt() + } else { + scale = replyButtonProgress + alpha = Math.min(255f, 255 * replyButtonProgress).toInt() + } + + imageDrawable.alpha = alpha + if (startTracking) { + if (!isVibrate && translationX >= triggerDistance) { + itemView.performHapticFeedback( + HapticFeedbackConstants.LONG_PRESS +// , HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING + ) + isVibrate = true + } + } + + val x: Int = itemView.width - if (translationX > triggerDistance + triggerDelta) { + (convertToPx(130) / 2).toInt() + } else { + (translationX / 2).toInt() + } + + val y = (itemView.top + itemView.measuredHeight / 2).toFloat() + //magic numbers? + imageDrawable.setBounds( + (x - convertToPx(12) * scale).toInt(), + (y - convertToPx(11) * scale).toInt(), + (x + convertToPx(12) * scale).toInt(), + (y + convertToPx(10) * scale).toInt() + ) + imageDrawable.draw(canvas) + imageDrawable.alpha = 255 + } + + private fun convertToPx(dp: Int): Float { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp.toFloat(), + context.resources.displayMetrics + ) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index 00f66f7c..b607b5a6 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -148,6 +148,7 @@ object VectorPreferences { private const val SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY = "SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY" private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" + private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" // analytics const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY" @@ -249,6 +250,10 @@ object VectorPreferences { return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false) } + fun swipeToReplyIsEnabled(context: Context): Boolean { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY, true) + } + /** * Tells if we have already asked the user to disable battery optimisations on android >= M devices. * diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 2b531bf4..7ad389d4 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -32,4 +32,5 @@ View the room directory + Enable swipe to reply in timeline \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index 66eb3f39..56205172 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -1,45 +1,49 @@ - + - - + + - + - - - + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - + + + + From 4bfaa00be486f064b4ba0e85b0d2ea356d46558a Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 17 Jul 2019 14:27:02 +0200 Subject: [PATCH 4/4] Fix / clean bad method name --- .../riotx/features/home/room/detail/RoomDetailFragment.kt | 2 +- .../home/room/detail/RoomMessageTouchHelperCallback.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 4f91539c..bfe3d2e1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -338,7 +338,7 @@ class RoomDetailFragment : } } - override fun canSwipeModelModel(model: EpoxyModel<*>): Boolean { + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { return when (model) { is MessageFileItem, is MessageImageVideoItem, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt index d716d6b4..2803c66b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt @@ -40,7 +40,7 @@ class RoomMessageTouchHelperCallback(private val context: Context, interface QuickReplayHandler { fun performQuickReplyOnHolder(model: EpoxyModel<*>) - fun canSwipeModelModel(model: EpoxyModel<*>): Boolean + fun canSwipeModel(model: EpoxyModel<*>): Boolean } private var swipeBack: Boolean = false @@ -67,7 +67,7 @@ class RoomMessageTouchHelperCallback(private val context: Context, } override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: EpoxyViewHolder): Int { - if (handler.canSwipeModelModel(viewHolder.model)) { + if (handler.canSwipeModel(viewHolder.model)) { return ItemTouchHelper.Callback.makeMovementFlags(0, ItemTouchHelper.START) //Should we use Left? } else { return 0