forked from GitHub-Mirror/riotX-android
Swipe to reply in timeline (lab)
This commit is contained in:
parent
9bdea5b325
commit
9494174c33
@ -7,6 +7,8 @@ Features:
|
|||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
- Handle click on redacted events: view source and create permalink
|
- 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:
|
Other changes:
|
||||||
-
|
-
|
||||||
|
@ -37,9 +37,11 @@ import androidx.annotation.DrawableRes
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import butterknife.BindView
|
import butterknife.BindView
|
||||||
|
import com.airbnb.epoxy.EpoxyModel
|
||||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.args
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
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.EditAggregatedSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
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.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.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
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.TimelineEventController
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.*
|
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.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.EventHtmlRenderer
|
||||||
import im.vector.riotx.features.html.PillImageSpan
|
import im.vector.riotx.features.html.PillImageSpan
|
||||||
import im.vector.riotx.features.invite.VectorInviteView
|
import im.vector.riotx.features.invite.VectorInviteView
|
||||||
@ -323,6 +326,32 @@ class RoomDetailFragment :
|
|||||||
})
|
})
|
||||||
recyclerView.setController(timelineEventController)
|
recyclerView.setController(timelineEventController)
|
||||||
timelineEventController.callback = this
|
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() {
|
private fun setupComposer() {
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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_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_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
|
// analytics
|
||||||
const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY"
|
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)
|
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.
|
* Tells if we have already asked the user to disable battery optimisations on android >= M devices.
|
||||||
*
|
*
|
||||||
|
@ -32,4 +32,5 @@
|
|||||||
<string name="room_filtering_footer_open_room_directory">View the room directory</string>
|
<string name="room_filtering_footer_open_room_directory">View the room directory</string>
|
||||||
|
|
||||||
|
|
||||||
|
<string name="labs_swipe_to_reply_in_timeline">Enable swipe to reply in timeline</string>
|
||||||
</resources>
|
</resources>
|
@ -1,7 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<!--<im.vector.riotx.core.preference.VectorPreferenceCategory-->
|
<!--<im.vector.riotx.core.preference.VectorPreferenceCategory-->
|
||||||
<!--android:key="SETTINGS_LABS_PREFERENCE_KEY"-->
|
<!--android:key="SETTINGS_LABS_PREFERENCE_KEY"-->
|
||||||
@ -37,10 +35,16 @@
|
|||||||
<!--android:title="@string/settings_labs_enable_send_voice" />-->
|
<!--android:title="@string/settings_labs_enable_send_voice" />-->
|
||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorSwitchPreference
|
<im.vector.riotx.core.preference.VectorSwitchPreference
|
||||||
android:key="SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
|
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
|
android:key="SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
|
||||||
android:title="@string/settings_labs_show_hidden_events_in_timeline" />
|
android:title="@string/settings_labs_show_hidden_events_in_timeline" />
|
||||||
|
|
||||||
|
|
||||||
|
<im.vector.riotx.core.preference.VectorSwitchPreference
|
||||||
|
android:defaultValue="true"
|
||||||
|
android:key="SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
|
||||||
|
android:title="@string/labs_swipe_to_reply_in_timeline" />
|
||||||
|
|
||||||
<!--</im.vector.riotx.core.preference.VectorPreferenceCategory>-->
|
<!--</im.vector.riotx.core.preference.VectorPreferenceCategory>-->
|
||||||
|
|
||||||
</androidx.preference.PreferenceScreen>
|
</androidx.preference.PreferenceScreen>
|
Loading…
x
Reference in New Issue
Block a user