mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
View all threads screen implementation & UI
Add user friendly message thread summary on the SDK side Fix not encrypted rooms thread summaries
This commit is contained in:
parent
586b3d8caa
commit
722f367690
@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.Room
|
import org.matrix.android.sdk.api.session.room.Room
|
||||||
|
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
||||||
import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
|
import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
|
||||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||||
@ -98,6 +99,13 @@ class FlowRoom(private val room: Room) {
|
|||||||
fun liveNotificationState(): Flow<RoomNotificationState> {
|
fun liveNotificationState(): Flow<RoomNotificationState> {
|
||||||
return room.getLiveRoomNotificationState().asFlow()
|
return room.getLiveRoomNotificationState().asFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun liveThreadList(): Flow<List<TimelineEvent>> {
|
||||||
|
return room.getAllThreadsLive().asFlow()
|
||||||
|
.startWith(room.coroutineDispatchers.io) {
|
||||||
|
room.getAllThreads()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Room.flow(): FlowRoom {
|
fun Room.flow(): FlowRoom {
|
||||||
|
@ -27,11 +27,13 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
|||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.api.session.threads.ThreadDetails
|
import org.matrix.android.sdk.api.session.threads.ThreadDetails
|
||||||
|
import org.matrix.android.sdk.api.util.ContentUtils
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
import org.matrix.android.sdk.internal.session.presence.model.PresenceContent
|
import org.matrix.android.sdk.internal.session.presence.model.PresenceContent
|
||||||
|
import org.matrix.android.sdk.internal.session.room.send.removeInReplyFallbacks
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
typealias Content = JsonDict
|
typealias Content = JsonDict
|
||||||
@ -188,14 +190,39 @@ data class Event(
|
|||||||
return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
|
return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDecryptedMessageText(): String {
|
/**
|
||||||
return getValueFromPayload(mxDecryptionResult?.payload).orEmpty()
|
* Returns a user friendly content depending on the message type.
|
||||||
|
* It can be used especially for message summaries.
|
||||||
|
* It will return a decrypted text message or an empty string otherwise.
|
||||||
|
*/
|
||||||
|
fun getDecryptedUserFriendlyTextSummary(): String {
|
||||||
|
val text = getDecryptedValue().orEmpty()
|
||||||
|
return when {
|
||||||
|
isReply() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
|
||||||
|
isFileMessage() -> "sent a file."
|
||||||
|
isAudioMessage() -> "sent an audio file."
|
||||||
|
isImageMessage() -> "sent an image."
|
||||||
|
isVideoMessage() -> "sent a video."
|
||||||
|
else -> text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
private fun Event.isQuote(): Boolean {
|
||||||
private fun getValueFromPayload(payload: JsonDict?, key: String = "body"): String? {
|
if (isReply()) return false
|
||||||
val content = payload?.get("content") as? JsonDict
|
return getDecryptedValue("formatted_body")?.contains("<blockquote>") ?: false
|
||||||
return content?.get(key) as? String
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt the message, or return the pure payload value if there is no encryption
|
||||||
|
*/
|
||||||
|
private fun getDecryptedValue(key: String = "body"): String? {
|
||||||
|
return if (isEncrypted()) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val content = mxDecryptionResult?.payload?.get("content") as? JsonDict
|
||||||
|
content?.get(key) as? String
|
||||||
|
} else {
|
||||||
|
content?.get(key) as? String
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,4 +55,17 @@ interface TimelineService {
|
|||||||
* Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO.
|
* Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO.
|
||||||
*/
|
*/
|
||||||
fun getAttachmentMessages(): List<TimelineEvent>
|
fun getAttachmentMessages(): List<TimelineEvent>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a live list of all the thread for the specified roomId
|
||||||
|
* @return the [LiveData] of [TimelineEvent]
|
||||||
|
*/
|
||||||
|
fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of all the thread for the specified roomId
|
||||||
|
* @return the [LiveData] of [TimelineEvent]
|
||||||
|
*/
|
||||||
|
fun getAllThreads(): List<TimelineEvent>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package org.matrix.android.sdk.internal.database.helper
|
package org.matrix.android.sdk.internal.database.helper
|
||||||
|
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmQuery
|
||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
import io.realm.Sort
|
import io.realm.Sort
|
||||||
import org.matrix.android.sdk.BuildConfig
|
import org.matrix.android.sdk.BuildConfig
|
||||||
@ -82,7 +83,17 @@ internal fun EventEntity.findAllThreadsForRootEventId(realm: Realm, rootThreadEv
|
|||||||
TimelineEventEntity
|
TimelineEventEntity
|
||||||
.whereRoomId(realm, roomId = roomId)
|
.whereRoomId(realm, roomId = roomId)
|
||||||
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
||||||
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
|
.findAll()
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all TimelineEventEntity that are root threads for the specified room
|
||||||
|
* @param roomId The room that all stored root threads will be returned
|
||||||
|
*/
|
||||||
|
internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<TimelineEventEntity> =
|
||||||
|
TimelineEventEntity
|
||||||
|
.whereRoomId(realm, roomId = roomId)
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD,true)
|
||||||
|
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
|
|
||||||
|
@ -22,11 +22,9 @@ import org.matrix.android.sdk.api.session.events.model.Event
|
|||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
||||||
import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
|
import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
|
||||||
import org.matrix.android.sdk.api.session.events.model.isThread
|
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||||
import org.matrix.android.sdk.api.session.threads.ThreadDetails
|
import org.matrix.android.sdk.api.session.threads.ThreadDetails
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
@ -113,7 +111,7 @@ internal object EventMapper {
|
|||||||
avatarUrl = timelineEventEntity.senderAvatar
|
avatarUrl = timelineEventEntity.senderAvatar
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedMessageText().orEmpty()
|
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedUserFriendlyTextSummary().orEmpty()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,7 +168,11 @@ internal class DefaultTimeline(
|
|||||||
TimelineEventEntity
|
TimelineEventEntity
|
||||||
.whereRoomId(realm, roomId = roomId)
|
.whereRoomId(realm, roomId = roomId)
|
||||||
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it)
|
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it)
|
||||||
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
.or()
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, rootThreadEventId)
|
||||||
|
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
|
.findAll()
|
||||||
|
|
||||||
} ?: buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
} ?: buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
||||||
|
|
||||||
timelineEvents.addChangeListener(eventsChangeListener)
|
timelineEvents.addChangeListener(eventsChangeListener)
|
||||||
|
@ -31,9 +31,11 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
|||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||||
|
import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
|
||||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
|
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
|
||||||
@ -102,4 +104,17 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
|||||||
.orEmpty()
|
.orEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
|
||||||
|
return monarchy.findAllMappedWithChanges(
|
||||||
|
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
|
||||||
|
{ timelineEventMapper.map(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override fun getAllThreads(): List<TimelineEvent> {
|
||||||
|
return monarchy.fetchAllMappedSync(
|
||||||
|
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
|
||||||
|
{ timelineEventMapper.map(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/coordinatorLayout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -58,7 +58,7 @@ import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
|
|||||||
import im.vector.app.features.home.room.detail.TimelineFragment
|
import im.vector.app.features.home.room.detail.TimelineFragment
|
||||||
import im.vector.app.features.home.room.detail.search.SearchFragment
|
import im.vector.app.features.home.room.detail.search.SearchFragment
|
||||||
import im.vector.app.features.home.room.list.RoomListFragment
|
import im.vector.app.features.home.room.list.RoomListFragment
|
||||||
import im.vector.app.features.home.room.threads.detail.ThreadListFragment
|
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
|
||||||
import im.vector.app.features.login.LoginCaptchaFragment
|
import im.vector.app.features.login.LoginCaptchaFragment
|
||||||
import im.vector.app.features.login.LoginFragment
|
import im.vector.app.features.login.LoginFragment
|
||||||
import im.vector.app.features.login.LoginGenericTextInputFormFragment
|
import im.vector.app.features.login.LoginGenericTextInputFormFragment
|
||||||
|
@ -68,6 +68,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
import com.jakewharton.rxbinding3.view.focusChanges
|
import com.jakewharton.rxbinding3.view.focusChanges
|
||||||
import com.jakewharton.rxbinding3.widget.textChanges
|
import com.jakewharton.rxbinding3.widget.textChanges
|
||||||
import com.vanniktech.emoji.EmojiPopup
|
import com.vanniktech.emoji.EmojiPopup
|
||||||
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
|
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
|
||||||
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
||||||
@ -971,7 +972,7 @@ class TimelineFragment @Inject constructor(
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.threads -> {
|
R.id.threads -> {
|
||||||
requireActivity().toast("View All Threads")
|
navigateToThreadList()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.search -> {
|
R.id.search -> {
|
||||||
@ -1776,7 +1777,7 @@ class TimelineFragment @Inject constructor(
|
|||||||
roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId))
|
roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isRootThreadEvent) {
|
if (BuildConfig.THREADING_ENABLED && isRootThreadEvent && !isThreadTimeLine()) {
|
||||||
navigateToThreadTimeline(informationData.eventId)
|
navigateToThreadTimeline(informationData.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2136,8 +2137,8 @@ class TimelineFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to Threads timeline for the specified threadRootEventId
|
* Navigate to Threads timeline for the specified rootThreadEventId
|
||||||
* using the RoomThreadDetailActivity
|
* using the ThreadsActivity
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private fun navigateToThreadTimeline(rootThreadEventId: String) {
|
private fun navigateToThreadTimeline(rootThreadEventId: String) {
|
||||||
@ -2151,6 +2152,21 @@ class TimelineFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to Threads list for the current room
|
||||||
|
* using the ThreadsActivity
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun navigateToThreadList() {
|
||||||
|
context?.let {
|
||||||
|
val roomThreadDetailArgs = ThreadTimelineArgs(
|
||||||
|
roomId = timelineArgs.roomId,
|
||||||
|
displayName = roomDetailViewModel.getRoomSummary()?.displayName,
|
||||||
|
avatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl)
|
||||||
|
navigator.openThreadList(it, roomThreadDetailArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// VectorInviteView.Callback
|
// VectorInviteView.Callback
|
||||||
|
|
||||||
override fun onAcceptInvite() {
|
override fun onAcceptInvite() {
|
||||||
|
@ -27,6 +27,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
|||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.epoxy.ClickListener
|
import im.vector.app.core.epoxy.ClickListener
|
||||||
import im.vector.app.core.epoxy.onClick
|
import im.vector.app.core.epoxy.onClick
|
||||||
@ -105,14 +106,17 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||||||
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
|
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
|
||||||
|
|
||||||
// Threads
|
// Threads
|
||||||
attributes.threadDetails?.let { threadDetails ->
|
if(BuildConfig.THREADING_ENABLED) {
|
||||||
threadDetails.isRootThread
|
attributes.threadDetails?.let { threadDetails ->
|
||||||
holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread
|
holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread
|
||||||
holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString()
|
holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString()
|
||||||
holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage
|
holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage
|
||||||
threadDetails.threadSummarySenderInfo?.let { senderInfo ->
|
threadDetails.threadSummarySenderInfo?.let { senderInfo ->
|
||||||
attributes.avatarRenderer.render(MatrixItem.UserItem(senderInfo.userId, senderInfo.displayName, senderInfo.avatarUrl), holder.threadSummaryAvatarImageView)
|
attributes.avatarRenderer.render(MatrixItem.UserItem(senderInfo.userId, senderInfo.displayName, senderInfo.avatarUrl), holder.threadSummaryAvatarImageView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}else{
|
||||||
|
holder.threadSummaryConstraintLayout.isVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,14 +25,13 @@ import im.vector.app.core.di.ScreenComponent
|
|||||||
import im.vector.app.core.extensions.replaceFragment
|
import im.vector.app.core.extensions.replaceFragment
|
||||||
import im.vector.app.core.platform.ToolbarConfigurable
|
import im.vector.app.core.platform.ToolbarConfigurable
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
|
||||||
import im.vector.app.databinding.ActivityThreadsBinding
|
import im.vector.app.databinding.ActivityThreadsBinding
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
|
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
|
||||||
import im.vector.app.features.home.room.detail.TimelineFragment
|
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.ThreadListArgs
|
||||||
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
|
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
|
||||||
import im.vector.app.features.home.room.threads.detail.ThreadListFragment
|
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ThreadsActivity : VectorBaseActivity<ActivityThreadsBinding>(), ToolbarConfigurable {
|
class ThreadsActivity : VectorBaseActivity<ActivityThreadsBinding>(), ToolbarConfigurable {
|
||||||
|
@ -21,5 +21,7 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ThreadListArgs(
|
data class ThreadListArgs(
|
||||||
val roomId: String
|
val roomId: String,
|
||||||
|
val displayName: String?,
|
||||||
|
val avatarUrl: String?,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 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.app.features.home.room.threads.detail
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import com.airbnb.mvrx.args
|
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
|
||||||
import im.vector.app.databinding.FragmentThreadListBinding
|
|
||||||
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
|
|
||||||
import org.matrix.android.sdk.api.session.Session
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class ThreadListFragment @Inject constructor(
|
|
||||||
private val session: Session
|
|
||||||
) : VectorBaseFragment<FragmentThreadListBinding>() {
|
|
||||||
|
|
||||||
private val threadTimelineArgs: ThreadTimelineArgs by args()
|
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentThreadListBinding {
|
|
||||||
return FragmentThreadListBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
initTextComposer()
|
|
||||||
// lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
// Realm.getInstance(realmConfiguration).executeTransaction {
|
|
||||||
// val eventId = roomThreadDetailArgs.eventId ?: return@executeTransaction
|
|
||||||
// val r = EventEntity.where(it, eventId = eventId)
|
|
||||||
// .findFirst() ?: return@executeTransaction
|
|
||||||
// Timber.i("------> $eventId isThread: ${EventMapper.map(r).isThread()}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//// views.testTextVeiwddasda.text = "${roomThreadDetailArgs.eventId} -- ${roomThreadDetailArgs.roomId}"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initTextComposer(){
|
|
||||||
// views.roomThreadDetailTextComposerView.views.sendButton.isVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* 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.app.features.home.room.threads.list.model
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.app.features.displayname.getBestName
|
||||||
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_thread_summary)
|
||||||
|
abstract class ThreadSummaryModel : VectorEpoxyModel<ThreadSummaryModel.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
@EpoxyAttribute lateinit var matrixItem: MatrixItem
|
||||||
|
@EpoxyAttribute lateinit var title: String
|
||||||
|
@EpoxyAttribute lateinit var date: String
|
||||||
|
@EpoxyAttribute lateinit var rootMessage: String
|
||||||
|
@EpoxyAttribute lateinit var lastMessage: String
|
||||||
|
@EpoxyAttribute lateinit var lastMessageCounter: String
|
||||||
|
@EpoxyAttribute lateinit var lastMessageMatrixItem: MatrixItem
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
||||||
|
holder.avatarImageView.contentDescription = matrixItem.getBestName()
|
||||||
|
holder.titleTextView.text = title
|
||||||
|
holder.dateTextView.text = date
|
||||||
|
holder.rootMessageTextView.text = rootMessage
|
||||||
|
|
||||||
|
// Last message summary
|
||||||
|
avatarRenderer.render(lastMessageMatrixItem, holder.lastMessageAvatarImageView)
|
||||||
|
holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem.getBestName()
|
||||||
|
holder.lastMessageTextView.text = lastMessage
|
||||||
|
holder.lastMessageCounterTextView.text = lastMessageCounter
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val avatarImageView by bind<ImageView>(R.id.threadSummaryAvatarImageView)
|
||||||
|
val titleTextView by bind<TextView>(R.id.threadSummaryTitleTextView)
|
||||||
|
val dateTextView by bind<TextView>(R.id.threadSummaryDateTextView)
|
||||||
|
val rootMessageTextView by bind<TextView>(R.id.threadSummaryRootMessageTextView)
|
||||||
|
val lastMessageAvatarImageView by bind<ImageView>(R.id.messageThreadSummaryAvatarImageView)
|
||||||
|
val lastMessageCounterTextView by bind<TextView>(R.id.messageThreadSummaryCounterTextView)
|
||||||
|
val lastMessageTextView by bind<TextView>(R.id.messageThreadSummaryInfoTextView)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 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.app.features.home.room.threads.list.viewmodel
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.EpoxyController
|
||||||
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
import im.vector.app.features.home.room.threads.list.model.threadSummary
|
||||||
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ThreadSummaryController @Inject constructor(
|
||||||
|
private val avatarRenderer: AvatarRenderer
|
||||||
|
) : EpoxyController() {
|
||||||
|
|
||||||
|
var listener: Listener? = null
|
||||||
|
|
||||||
|
private var viewState: ThreadSummaryViewState? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
// We are requesting a model build directly as the first build of epoxy is on the main thread.
|
||||||
|
// It avoids to build the whole list of breadcrumbs on the main thread.
|
||||||
|
requestModelBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(viewState: ThreadSummaryViewState) {
|
||||||
|
this.viewState = viewState
|
||||||
|
requestModelBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildModels() {
|
||||||
|
val safeViewState = viewState ?: return
|
||||||
|
val host = this
|
||||||
|
// Add a ZeroItem to avoid automatic scroll when the breadcrumbs are updated from another client
|
||||||
|
// zeroItem {
|
||||||
|
// id("top")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// An empty breadcrumbs list can only be temporary because when entering in a room,
|
||||||
|
// this one is added to the breadcrumbs
|
||||||
|
safeViewState.rootThreadEventList.invoke()
|
||||||
|
?.forEach { timelineEvent ->
|
||||||
|
threadSummary {
|
||||||
|
id(timelineEvent.eventId)
|
||||||
|
avatarRenderer(host.avatarRenderer)
|
||||||
|
matrixItem(timelineEvent.senderInfo.toMatrixItem())
|
||||||
|
title(timelineEvent.senderInfo.displayName)
|
||||||
|
date(timelineEvent.root.ageLocalTs.toString())
|
||||||
|
rootMessage(timelineEvent.root.getDecryptedUserFriendlyTextSummary())
|
||||||
|
lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty())
|
||||||
|
lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())
|
||||||
|
lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onBreadcrumbClicked(roomId: String)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 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.app.features.home.room.threads.list.viewmodel
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.core.platform.EmptyAction
|
||||||
|
import im.vector.app.core.platform.EmptyViewEvents
|
||||||
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
|
||||||
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.room.Room
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
|
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||||
|
import org.matrix.android.sdk.flow.flow
|
||||||
|
|
||||||
|
class ThreadSummaryViewModel @AssistedInject constructor(@Assisted val initialState: ThreadSummaryViewState,
|
||||||
|
private val session: Session) :
|
||||||
|
VectorViewModel<ThreadSummaryViewState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||||
|
|
||||||
|
private val room = session.getRoom(initialState.roomId)
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: ThreadSummaryViewState): ThreadSummaryViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MavericksViewModelFactory<ThreadSummaryViewModel, ThreadSummaryViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: ThreadSummaryViewState): ThreadSummaryViewModel? {
|
||||||
|
val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
|
return fragment.threadSummaryViewModelFactory.create(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
observeThreadsSummary()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: EmptyAction) {
|
||||||
|
// No op
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun observeThreadsSummary() {
|
||||||
|
room?.flow()
|
||||||
|
?.liveThreadList()
|
||||||
|
?.execute { asyncThreads ->
|
||||||
|
copy(rootThreadEventList = asyncThreads)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 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.app.features.home.room.threads.list.viewmodel
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.Async
|
||||||
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
|
data class ThreadSummaryViewState(
|
||||||
|
val rootThreadEventList: Async<List<TimelineEvent>> = Uninitialized,
|
||||||
|
val roomId: String
|
||||||
|
) : MavericksState{
|
||||||
|
|
||||||
|
constructor(args: ThreadListArgs) : this(roomId = args.roomId)
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.app.features.home.room.threads.list.views
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.airbnb.mvrx.args
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.cleanup
|
||||||
|
import im.vector.app.core.extensions.configureWith
|
||||||
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.app.databinding.FragmentThreadListBinding
|
||||||
|
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.arguments.ThreadListArgs
|
||||||
|
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.util.MatrixItem
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ThreadListFragment @Inject constructor(
|
||||||
|
private val session: Session,
|
||||||
|
private val avatarRenderer: AvatarRenderer,
|
||||||
|
private val threadSummaryController: ThreadSummaryController,
|
||||||
|
val threadSummaryViewModelFactory: ThreadSummaryViewModel.Factory
|
||||||
|
) : VectorBaseFragment<FragmentThreadListBinding>() {
|
||||||
|
|
||||||
|
private val threadSummaryViewModel: ThreadSummaryViewModel by fragmentViewModel()
|
||||||
|
|
||||||
|
private val threadListArgs: ThreadListArgs by args()
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentThreadListBinding {
|
||||||
|
return FragmentThreadListBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMenuRes() = R.menu.menu_thread_list
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
initToolbar()
|
||||||
|
views.threadListRecyclerView.configureWith(threadSummaryController, BreadcrumbsAnimator(), hasFixedSize = false)
|
||||||
|
// threadSummaryController.listener = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
views.threadListRecyclerView.cleanup()
|
||||||
|
// breadcrumbsController.listener = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
private fun initToolbar(){
|
||||||
|
setupToolbar(views.threadListToolbar)
|
||||||
|
renderToolbar()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(threadSummaryViewModel) { state ->
|
||||||
|
threadSummaryController.update(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -517,4 +517,14 @@ class DefaultNavigator @Inject constructor(
|
|||||||
threadTimelineArgs = threadTimelineArgs,
|
threadTimelineArgs = threadTimelineArgs,
|
||||||
threadListArgs =null))
|
threadListArgs =null))
|
||||||
}
|
}
|
||||||
|
override fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) {
|
||||||
|
context.startActivity(ThreadsActivity.newIntent(
|
||||||
|
context = context,
|
||||||
|
threadTimelineArgs = null,
|
||||||
|
threadListArgs = ThreadListArgs(
|
||||||
|
roomId = threadTimelineArgs.roomId,
|
||||||
|
displayName = threadTimelineArgs.displayName,
|
||||||
|
avatarUrl = threadTimelineArgs.avatarUrl
|
||||||
|
)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,5 +143,6 @@ interface Navigator {
|
|||||||
fun openCallTransfer(context: Context, callId: String)
|
fun openCallTransfer(context: Context, callId: String)
|
||||||
|
|
||||||
fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs)
|
fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs)
|
||||||
|
fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
40
vector/src/main/res/layout/fragment_thread_list.xml
Normal file
40
vector/src/main/res/layout/fragment_thread_list.xml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/threadListAppBarLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/threadListToolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?actionBarSize"
|
||||||
|
android:transitionName="toolbar">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/includeThreadListToolbar"
|
||||||
|
layout="@layout/view_room_detail_thread_toolbar" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/threadListRecyclerView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/threadListAppBarLayout"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
tools:listitem="@layout/item_thread_summary" />
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
91
vector/src/main/res/layout/item_thread_summary.xml
Normal file
91
vector/src/main/res/layout/item_thread_summary.xml
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<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:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingEnd="0dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/threadSummaryAvatarImageView"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/avatar"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@sample/user_round_avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/threadSummaryTitleTextView"
|
||||||
|
style="@style/Widget.Vector.TextView.Body.Medium"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/element_name_04"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/threadSummaryDateTextView"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/threadSummaryAvatarImageView"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Aris" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/threadSummaryDateTextView"
|
||||||
|
style="@style/Widget.Vector.TextView.Caption"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?vctr_content_secondary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/threadSummaryTitleTextView"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/threadSummaryTitleTextView"
|
||||||
|
tools:text="10 minutes ago" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/threadSummaryRootMessageTextView"
|
||||||
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="?vctr_content_primary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/threadSummaryTitleTextView"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/threadSummaryTitleTextView"
|
||||||
|
tools:text="It’s really encouraging to feel like you are a part of something greater than yourself. I It’s really encouraging to feel like you are a part of something greater than yourself. I " />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/threadSummaryConstraintLayout"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:contentDescription="@string/room_threads_filter"
|
||||||
|
android:maxWidth="496dp"
|
||||||
|
android:minWidth="144dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/threadSummaryTitleTextView"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/threadSummaryRootMessageTextView"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<include layout="@layout/view_thread_room_summary" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?vctr_content_quinary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/threadSummaryConstraintLayout"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/threadSummaryConstraintLayout" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -200,7 +200,27 @@
|
|||||||
</com.google.android.flexbox.FlexboxLayout>
|
</com.google.android.flexbox.FlexboxLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<include
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
layout="@layout/view_thread_room_summary" />
|
android:id="@+id/messageThreadSummaryConstraintLayout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/informationBottom"
|
||||||
|
android:layout_marginEnd="32dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:paddingStart="13dp"
|
||||||
|
android:paddingEnd="13dp"
|
||||||
|
android:layout_toEndOf="@id/messageStartGuideline"
|
||||||
|
android:background="@drawable/rounded_rect_shape_8"
|
||||||
|
android:contentDescription="@string/room_threads_filter"
|
||||||
|
android:maxWidth="496dp"
|
||||||
|
android:minWidth="144dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<include layout="@layout/view_thread_room_summary" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
@ -1,74 +1,58 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/messageThreadSummaryConstraintLayout"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="wrap_content"
|
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/informationBottom"
|
|
||||||
android:layout_marginEnd="32dp"
|
|
||||||
android:layout_marginBottom="4dp"
|
|
||||||
android:layout_toEndOf="@id/messageStartGuideline"
|
|
||||||
android:background="@drawable/rounded_rect_shape_8"
|
|
||||||
android:contentDescription="@string/room_threads_filter"
|
|
||||||
android:maxWidth="496dp"
|
|
||||||
android:minWidth="144dp"
|
|
||||||
android:paddingTop="10dp"
|
|
||||||
android:paddingBottom="10dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/messageThreadSummaryImageView"
|
android:id="@+id/messageThreadSummaryImageView"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
android:layout_width="16dp"
|
android:layout_width="16dp"
|
||||||
android:layout_height="16dp"
|
android:layout_height="16dp"
|
||||||
android:layout_marginStart="13dp"
|
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
android:contentDescription="@string/room_threads_filter"
|
android:contentDescription="@string/room_threads_filter"
|
||||||
android:src="@drawable/ic_thread_summary" />
|
android:src="@drawable/ic_thread_summary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/messageThreadSummaryCounterTextView"
|
android:id="@+id/messageThreadSummaryCounterTextView"
|
||||||
style="@style/Widget.Vector.TextView.Caption"
|
style="@style/Widget.Vector.TextView.Caption"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/messageThreadSummaryImageView"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="5dp"
|
||||||
android:textColor="?vctr_content_secondary"
|
android:textColor="?vctr_content_secondary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/messageThreadSummaryImageView"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="187" />
|
tools:text="187" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/messageThreadSummaryAvatarImageView"
|
android:id="@+id/messageThreadSummaryAvatarImageView"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/messageThreadSummaryCounterTextView"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:layout_marginStart="13dp"
|
android:layout_marginStart="13dp"
|
||||||
android:contentDescription="@string/avatar"
|
android:contentDescription="@string/avatar"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/messageThreadSummaryCounterTextView"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:src="@sample/user_round_avatars" />
|
tools:src="@sample/user_round_avatars" />
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/messageThreadSummaryInfoTextView"
|
android:id="@+id/messageThreadSummaryInfoTextView"
|
||||||
style="@style/Widget.Vector.TextView.Caption"
|
style="@style/Widget.Vector.TextView.Caption"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?vctr_content_secondary"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintStart_toEndOf="@id/messageThreadSummaryAvatarImageView"
|
app:layout_constraintStart_toEndOf="@id/messageThreadSummaryAvatarImageView"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintWidth_default="wrap"
|
app:layout_constraintWidth_default="wrap"
|
||||||
android:layout_width="0dp"
|
tools:text="Hello There, whats up! Its a large centence, whats up! Its a large centence" />
|
||||||
android:layout_height="wrap_content"
|
</merge>
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginEnd="13dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textColor="?vctr_content_secondary"
|
|
||||||
tools:text="Hello There, whats up! Its a large centence" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
<?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_filter"
|
|
||||||
android:icon="@drawable/ic_filter"
|
|
||||||
android:title="@string/room_threads_filter"
|
|
||||||
app:showAsAction="always" />
|
|
||||||
</menu>
|
|
13
vector/src/main/res/menu/menu_thread_list.xml
Normal file
13
vector/src/main/res/menu/menu_thread_list.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_thread_list_filter"
|
||||||
|
android:icon="@drawable/ic_filter"
|
||||||
|
android:title="@string/room_threads_filter"
|
||||||
|
app:iconTint="?vctr_content_secondary"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
</menu>
|
@ -1,36 +0,0 @@
|
|||||||
<?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/test"
|
|
||||||
android:icon="@drawable/ic_search"
|
|
||||||
android:title="@string/action_thread_view_in_room"
|
|
||||||
app:showAsAction="always">
|
|
||||||
|
|
||||||
<menu>
|
|
||||||
<item
|
|
||||||
android:id="@+id/menuWithIconText"
|
|
||||||
android:icon="@drawable/ic_thread_menu_item"
|
|
||||||
android:title="@string/action_thread_share"
|
|
||||||
/>
|
|
||||||
</menu>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_thread_timeline_view_in_room"
|
|
||||||
android:icon="@drawable/ic_settings_x"
|
|
||||||
android:title="@string/action_thread_view_in_room"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_thread_timeline_copy"
|
|
||||||
android:title="@string/action_thread_copy_link_to_thread"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_thread_timeline_share"
|
|
||||||
android:title="@string/action_thread_share"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
</menu>
|
|
Loading…
Reference in New Issue
Block a user