Add navigation to thread from the thread list

Add thread creation from photos, files, images, audios, replies etc
Add slide animation to view threads from thread list
Add click listener on room summary to handle cases like ( there is an image and the user upon click should view the image instead of navigating to the thread, so he can click the thread summary )
This commit is contained in:
ariskotsomitopoulos 2021-11-23 22:22:58 +02:00
parent 5e5ce614ef
commit e2bf3e7097
20 changed files with 149 additions and 42 deletions

View File

@ -1002,10 +1002,10 @@ class TimelineFragment @Inject constructor(
/**
* View and highlight the original root thread message in the main timeline
*/
private fun handleViewInRoomAction(){
private fun handleViewInRoomAction() {
getRootThreadEventId()?.let {
val newRoom = timelineArgs.copy(threadTimelineArgs = null,eventId = it)
context?.let{ con ->
val newRoom = timelineArgs.copy(threadTimelineArgs = null, eventId = it)
context?.let { con ->
val int = RoomDetailActivity.newIntent(con, newRoom)
int.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
con.startActivity(int)
@ -1473,6 +1473,7 @@ class TimelineFragment @Inject constructor(
avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView)
views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName
}
views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title)
}
}
@ -1776,9 +1777,9 @@ class TimelineFragment @Inject constructor(
is EncryptedEventContent -> {
roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId))
}
}
if (BuildConfig.THREADING_ENABLED && isRootThreadEvent && !isThreadTimeLine()) {
navigateToThreadTimeline(informationData.eventId)
else -> {
onThreadSummaryClicked(informationData.eventId, isRootThreadEvent)
}
}
}
@ -1809,6 +1810,13 @@ class TimelineFragment @Inject constructor(
}
}
override fun onThreadSummaryClicked(eventId: String, isRootThreadEvent: Boolean) {
if (BuildConfig.THREADING_ENABLED && isRootThreadEvent && !isThreadTimeLine()) {
navigateToThreadTimeline(eventId)
}
}
override fun onAvatarClicked(informationData: MessageInformationData) {
// roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.userId))
openRoomMemberProfile(informationData.senderId)
@ -2012,7 +2020,7 @@ class TimelineFragment @Inject constructor(
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
}
}
is EventSharedAction.ViewInRoom -> {
is EventSharedAction.ViewInRoom -> {
if (!views.voiceMessageRecorderView.isActive()) {
handleViewInRoomAction()
} else {

View File

@ -112,6 +112,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
BaseCallback,
ReactionPillCallback,
AvatarCallback,
ThreadCallback,
UrlClickCallback,
ReadReceiptsCallback,
PreviewUrlCallback {
@ -151,6 +152,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onMemberNameClicked(informationData: MessageInformationData)
}
interface ThreadCallback {
fun onThreadSummaryClicked(eventId: String, isRootThreadEvent: Boolean)
}
interface ReadReceiptsCallback {
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
fun onReadMarkerVisible()

View File

@ -435,13 +435,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
if (event.root.getClearType() != EventType.MESSAGE) return false
if (!actionPermissions.canSendMessage) return false
return when (messageContent?.msgType) {
MessageType.MSGTYPE_TEXT -> true
// MessageType.MSGTYPE_NOTICE,
// MessageType.MSGTYPE_EMOTE,
// MessageType.MSGTYPE_IMAGE,
// MessageType.MSGTYPE_VIDEO,
// MessageType.MSGTYPE_AUDIO,
// MessageType.MSGTYPE_FILE -> true
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE -> true
else -> false
}
}
@ -460,13 +460,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
if (!actionPermissions.canSendMessage) return false
return when (messageContent?.msgType) {
MessageType.MSGTYPE_TEXT -> event.root.threadDetails?.isRootThread ?: false
// MessageType.MSGTYPE_NOTICE,
// MessageType.MSGTYPE_EMOTE,
// MessageType.MSGTYPE_IMAGE,
// MessageType.MSGTYPE_VIDEO,
// MessageType.MSGTYPE_AUDIO,
// MessageType.MSGTYPE_FILE -> true
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE -> event.root.threadDetails?.isRootThread ?: false
else -> false
}
}

View File

@ -50,6 +50,7 @@ class MessageItemAttributesFactory @Inject constructor(
},
reactionPillCallback = callback,
avatarCallback = callback,
threadCallback = callback,
readReceiptsCallback = callback,
emojiTypeFace = emojiCompatFontProvider.typeface,
threadDetails = threadDetails

View File

@ -127,7 +127,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
val messageColorProvider: MessageColorProvider
val itemLongClickListener: View.OnLongClickListener?
val itemClickListener: ClickListener?
// val memberClickListener: ClickListener?
val reactionPillCallback: TimelineEventController.ReactionPillCallback?

View File

@ -67,6 +67,11 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
}
}
private val _threadClickListener = object : ClickListener {
override fun invoke(p1: View) {
attributes.threadCallback?.onThreadSummaryClicked(attributes.informationData.eventId, attributes.threadDetails?.isRootThread ?: false)
}
}
override fun bind(holder: H) {
super.bind(holder)
if (attributes.informationData.showInformation) {
@ -107,6 +112,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
// Threads
if(BuildConfig.THREADING_ENABLED) {
holder.threadSummaryConstraintLayout.onClick(_threadClickListener)
attributes.threadDetails?.let { threadDetails ->
holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread
holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString()
@ -125,6 +131,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
holder.avatarImageView.setOnLongClickListener(null)
holder.memberNameView.setOnClickListener(null)
holder.memberNameView.setOnLongClickListener(null)
holder.threadSummaryConstraintLayout.setOnClickListener(null)
super.unbind(holder)
}
@ -156,6 +163,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
val memberClickListener: ClickListener? = null,
override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
val avatarCallback: TimelineEventController.AvatarCallback? = null,
val threadCallback: TimelineEventController.ThreadCallback? = null,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
val emojiTypeFace: Typeface? = null,
val threadDetails: ThreadDetails? = null

View File

@ -77,7 +77,8 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
val noticeText: CharSequence,
val itemLongClickListener: View.OnLongClickListener? = null,
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
val avatarClickListener: ClickListener? = null
val avatarClickListener: ClickListener? = null,
val threadSummaryClickListener: ClickListener? = null
)
companion object {

View File

@ -19,9 +19,16 @@ package im.vector.app.features.home.room.threads
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.children
import androidx.fragment.app.FragmentTransaction
import com.google.android.material.appbar.MaterialToolbar
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity
@ -32,6 +39,7 @@ import im.vector.app.features.home.room.detail.TimelineFragment
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
class ThreadsActivity : VectorBaseActivity<ActivityThreadsBinding>(), ToolbarConfigurable {
@ -89,6 +97,31 @@ class ThreadsActivity : VectorBaseActivity<ActivityThreadsBinding>(), ToolbarCon
threadTimelineArgs = threadTimelineArgs
))
/**
* This function is used to navigate to the selected thread timeline.
* One usage of that is from the Threads Activity
*/
fun navigateToThreadTimeline(
timelineEvent: TimelineEvent) {
val roomThreadDetailArgs = ThreadTimelineArgs(
roomId = timelineEvent.roomId,
displayName = timelineEvent.senderInfo.displayName,
avatarUrl = timelineEvent.senderInfo.avatarUrl,
rootThreadEventId = timelineEvent.eventId)
val commonOption: (FragmentTransaction) -> Unit = {
it.setCustomAnimations(R.anim.animation_slide_in_right, R.anim.animation_slide_out_left, R.anim.animation_slide_in_left, R.anim.animation_slide_out_right)
}
addFragmentToBackstack(
frameId = R.id.threadsActivityFragmentContainer,
fragmentClass = TimelineFragment::class.java,
params = TimelineArgs(
roomId = timelineEvent.roomId,
threadTimelineArgs = roomThreadDetailArgs
),
option = commonOption
)
}
override fun configure(toolbar: MaterialToolbar) {
configureToolbar(toolbar)
}

View File

@ -16,13 +16,17 @@
package im.vector.app.features.home.room.threads.list.model
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.util.MatrixItem
@ -38,9 +42,11 @@ abstract class ThreadSummaryModel : VectorEpoxyModel<ThreadSummaryModel.Holder>(
@EpoxyAttribute lateinit var lastMessage: String
@EpoxyAttribute lateinit var lastMessageCounter: String
@EpoxyAttribute var lastMessageMatrixItem: MatrixItem? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: ClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.onClick(itemClickListener)
avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.avatarImageView.contentDescription = matrixItem.getBestName()
holder.titleTextView.text = title
@ -54,6 +60,7 @@ abstract class ThreadSummaryModel : VectorEpoxyModel<ThreadSummaryModel.Holder>(
holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem?.getBestName()
holder.lastMessageTextView.text = lastMessage
holder.lastMessageCounterTextView.text = lastMessageCounter
}
class Holder : VectorEpoxyHolder() {
@ -64,5 +71,6 @@ abstract class ThreadSummaryModel : VectorEpoxyModel<ThreadSummaryModel.Holder>(
val lastMessageAvatarImageView by bind<ImageView>(R.id.messageThreadSummaryAvatarImageView)
val lastMessageCounterTextView by bind<TextView>(R.id.messageThreadSummaryCounterTextView)
val lastMessageTextView by bind<TextView>(R.id.messageThreadSummaryInfoTextView)
val rootView by bind<ConstraintLayout>(R.id.threadSummaryRootConstraintLayout)
}
}

View File

@ -21,6 +21,7 @@ import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.threads.list.model.threadSummary
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@ -67,11 +68,14 @@ class ThreadSummaryController @Inject constructor(
lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty())
lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())
lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem())
itemClickListener {
host.listener?.onThreadClicked(timelineEvent)
}
}
}
}
interface Listener {
fun onBreadcrumbClicked(roomId: String)
fun onThreadClicked(timelineEvent: TimelineEvent)
}
}

View File

@ -20,7 +20,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.transition.TransitionInflater
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -33,10 +35,13 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsAnimator
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
import im.vector.app.features.home.room.detail.RoomDetailSharedActionViewModel
import im.vector.app.features.home.room.threads.ThreadsActivity
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryController
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewModel
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject
@ -45,7 +50,8 @@ class ThreadListFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val threadSummaryController: ThreadSummaryController,
val threadSummaryViewModelFactory: ThreadSummaryViewModel.Factory
) : VectorBaseFragment<FragmentThreadListBinding>() {
) : VectorBaseFragment<FragmentThreadListBinding>(),
ThreadSummaryController.Listener {
private val threadSummaryViewModel: ThreadSummaryViewModel by fragmentViewModel()
@ -65,15 +71,16 @@ class ThreadListFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState)
initToolbar()
views.threadListRecyclerView.configureWith(threadSummaryController, BreadcrumbsAnimator(), hasFixedSize = false)
// threadSummaryController.listener = this
threadSummaryController.listener = this
}
override fun onDestroyView() {
views.threadListRecyclerView.cleanup()
// breadcrumbsController.listener = null
threadSummaryController.listener = null
super.onDestroyView()
}
private fun initToolbar(){
private fun initToolbar() {
setupToolbar(views.threadListToolbar)
renderToolbar()
}
@ -84,8 +91,13 @@ class ThreadListFragment @Inject constructor(
private fun renderToolbar() {
views.includeThreadListToolbar.roomToolbarThreadConstraintLayout.isVisible = true
val matrixItem = MatrixItem.RoomItem(threadListArgs.roomId, threadListArgs.displayName, threadListArgs.avatarUrl)
avatarRenderer.render(matrixItem, views.includeThreadListToolbar.roomToolbarThreadImageView)
views.includeThreadListToolbar.roomToolbarThreadSubtitleTextView.text = threadListArgs.displayName
val matrixItem = MatrixItem.RoomItem(threadListArgs.roomId, threadListArgs.displayName, threadListArgs.avatarUrl)
avatarRenderer.render(matrixItem, views.includeThreadListToolbar.roomToolbarThreadImageView)
views.includeThreadListToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_list_title)
views.includeThreadListToolbar.roomToolbarThreadSubtitleTextView.text = threadListArgs.displayName
}
override fun onThreadClicked(timelineEvent: TimelineEvent) {
(activity as? ThreadsActivity)?.navigateToThreadTimeline(timelineEvent)
}
}

View File

@ -143,6 +143,7 @@ interface Navigator {
fun openCallTransfer(context: Context, callId: String)
fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs)
fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs)
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="-100%p" android:toXDelta="0"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%p" android:toXDelta="0"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="-100%p"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="100%p"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View File

@ -1,12 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:id="@+id/threadSummaryRootConstraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="0dp">
android:paddingEnd="0dp"
android:background="?android:colorBackground"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<ImageView
android:id="@+id/threadSummaryAvatarImageView"

View File

@ -5,8 +5,8 @@
android:id="@+id/roomToolbarThreadConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="gone"
android:visibility="gone">
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/roomToolbarThreadTitleTextView"
@ -15,11 +15,11 @@
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/room_threads_title"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/thread_timeline_title" />
<ImageView
android:id="@+id/roomToolbarThreadImageView"

View File

@ -20,8 +20,8 @@
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minEms="1"
android:layout_marginStart="5dp"
android:minEms="1"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/messageThreadSummaryImageView"
@ -43,17 +43,17 @@
<TextView
android:id="@+id/messageThreadSummaryInfoTextView"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?vctr_content_secondary"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/messageThreadSummaryAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap"
tools:text="Hello There, whats up! Its a large centence, whats up! Its a large centence" />
tools:text="Hello There, whats up! Its a large sentence whats up! Its a large centence" />
</merge>

View File

@ -1029,7 +1029,8 @@
<!-- Room Threads -->
<string name="room_threads_filter">Filter Threads in room</string>
<string name="room_threads_title">Thread</string>
<string name="thread_timeline_title">Thread</string>
<string name="thread_list_title">Threads</string>
<!-- Room events -->
<string name="room_event_action_report_prompt_reason">Reason for reporting this content</string>