Makes the timeline a little more polished

This commit is contained in:
ganfra 2018-10-31 13:03:36 +01:00
parent ecf728aef8
commit 6f2beef726
13 changed files with 179 additions and 70 deletions

View File

@ -0,0 +1,13 @@
package im.vector.riotredesign.core.extensions
import android.content.Context
import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat
import com.amulyakhare.textdrawable.TextDrawable
import im.vector.riotredesign.R
fun Context.avatarDrawable(name: String): Drawable {
val avatarColor = ContextCompat.getColor(this, R.color.pale_teal)
return TextDrawable.builder().buildRound(name.firstCharAsString().toUpperCase(), avatarColor)
}

View File

@ -12,10 +12,10 @@ import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.avatarDrawable
import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.utils.FragmentArgumentDelegate import im.vector.riotredesign.core.utils.FragmentArgumentDelegate
import im.vector.riotredesign.features.home.RoomSummaryViewHelper
import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.fragment_room_detail.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
@ -33,7 +33,7 @@ class RoomDetailFragment : RiotFragment() {
private val matrix by inject<Matrix>() private val matrix by inject<Matrix>()
private val currentSession = matrix.currentSession!! private val currentSession = matrix.currentSession!!
private var roomId by FragmentArgumentDelegate<String>() private var roomId by FragmentArgumentDelegate<String>()
private val timelineEventController = TimelineEventController() private lateinit var timelineEventController: TimelineEventController
private lateinit var room: Room private lateinit var room: Room
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -60,14 +60,14 @@ class RoomDetailFragment : RiotFragment() {
private fun setupRecyclerView() { private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
timelineEventController = TimelineEventController(riotActivity)
recyclerView.setController(timelineEventController) recyclerView.setController(timelineEventController)
} }
private fun renderRoomSummary(roomSummary: RoomSummary?) { private fun renderRoomSummary(roomSummary: RoomSummary?) {
roomSummary?.let { roomSummary?.let {
val roomSummaryViewHelper = RoomSummaryViewHelper(it)
toolbarTitleView.text = it.displayName toolbarTitleView.text = it.displayName
toolbarAvatarImageView.setImageDrawable(roomSummaryViewHelper.avatarDrawable(riotActivity)) toolbarAvatarImageView.setImageDrawable(riotActivity.avatarDrawable(it.displayName))
if (it.topic.isNotEmpty()) { if (it.topic.isNotEmpty()) {
toolbarSubtitleView.visibility = View.VISIBLE toolbarSubtitleView.visibility = View.VISIBLE
toolbarSubtitleView.text = it.topic toolbarSubtitleView.text = it.topic

View File

@ -1,22 +1,23 @@
package im.vector.riotredesign.features.home.room.detail package im.vector.riotredesign.features.home.room.detail
import android.arch.paging.PagedList import android.arch.paging.PagedList
import android.content.Context
import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.matrix.android.api.session.events.model.EnrichedEvent import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.roomMember
import im.vector.matrix.android.api.session.room.model.MessageContent import im.vector.matrix.android.api.session.room.model.MessageContent
import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.riotredesign.core.extensions.avatarDrawable
import im.vector.riotredesign.features.home.LoadingItemModel_ import im.vector.riotredesign.features.home.LoadingItemModel_
private const val PREFETCH_DISTANCE = 5 class TimelineEventController(private val context: Context) : EpoxyController(
class TimelineEventController : EpoxyController(
EpoxyAsyncUtil.getAsyncBackgroundHandler(), EpoxyAsyncUtil.getAsyncBackgroundHandler(),
EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyAsyncUtil.getAsyncBackgroundHandler()
) { ) {
private val messagesLoadedWithInformation = HashSet<String?>()
private val pagedListCallback = object : PagedList.Callback() { private val pagedListCallback = object : PagedList.Callback() {
override fun onChanged(position: Int, count: Int) { override fun onChanged(position: Int, count: Int) {
requestModelBuild() requestModelBuild()
@ -38,7 +39,6 @@ class TimelineEventController : EpoxyController(
field?.addWeakCallback(null, pagedListCallback) field?.addWeakCallback(null, pagedListCallback)
} }
override fun buildModels() { override fun buildModels() {
buildModels(timeline) buildModels(timeline)
} }
@ -47,22 +47,37 @@ class TimelineEventController : EpoxyController(
if (data.isNullOrEmpty()) { if (data.isNullOrEmpty()) {
return return
} }
data.forEachIndexed { index, enrichedEvent -> for (index in 0 until data.size) {
val item = if (enrichedEvent.root.type == EventType.MESSAGE) { val event = data[index]
val messageContent = enrichedEvent.root.content<MessageContent>() val nextEvent = if (index + 1 < data.size) data[index + 1] else null
val roomMember = enrichedEvent.getMetadata<Event>(EventType.STATE_ROOM_MEMBER)?.content<RoomMember>()
val title = "${roomMember?.displayName} : ${messageContent?.body}" if (event.root.type == EventType.MESSAGE) {
TimelineEventItem(title = title) val messageContent = event.root.content<MessageContent>()
} else { val roomMember = event.roomMember()
TimelineEventItem(title = enrichedEvent.toString()) if (messageContent == null || roomMember == null) {
continue
} }
item val nextRoomMember = nextEvent?.roomMember()
if (nextRoomMember != roomMember) {
messagesLoadedWithInformation.add(event.root.eventId)
}
val showInformation = messagesLoadedWithInformation.contains(event.root.eventId)
val avatarDrawable = context.avatarDrawable(roomMember.displayName ?: "")
TimelineMessageItem(
message = messageContent.body,
showInformation = showInformation,
avatarDrawable = avatarDrawable,
memberName = roomMember.displayName
)
.onBind { timeline?.loadAround(index) } .onBind { timeline?.loadAround(index) }
.id(enrichedEvent.root.eventId) .id(event.root.eventId)
.addTo(this) .addTo(this)
} }
}
val isLastEvent = data.last().getMetadata<Boolean>(EnrichedEvent.IS_LAST_EVENT) ?: false //It's a hack at the moment
val isLastEvent = data.last().root.type == EventType.STATE_ROOM_CREATE
LoadingItemModel_() LoadingItemModel_()
.id("backward_loading_item") .id("backward_loading_item")
.addIf(!isLastEvent, this) .addIf(!isLastEvent, this)

View File

@ -0,0 +1,39 @@
package im.vector.riotredesign.features.home.room.detail
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel
data class TimelineMessageItem(
val message: CharSequence? = null,
val time: CharSequence? = null,
val avatarDrawable: Drawable? = null,
val memberName: CharSequence? = null,
val showInformation: Boolean = true
) : KotlinModel(R.layout.item_event_message) {
private val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
private val memberNameView by bind<TextView>(R.id.messageMemberNameView)
private val timeView by bind<TextView>(R.id.messageTimeView)
private val messageView by bind<TextView>(R.id.messageTextView)
override fun bind() {
messageView.text = message
if (showInformation) {
avatarImageView.visibility = View.VISIBLE
memberNameView.visibility = View.VISIBLE
timeView.visibility = View.VISIBLE
avatarImageView.setImageDrawable(avatarDrawable)
timeView.text = time
memberNameView.text = memberName
} else {
avatarImageView.visibility = View.GONE
memberNameView.visibility = View.GONE
timeView.visibility = View.GONE
}
}
}

View File

@ -3,8 +3,7 @@ package im.vector.riotredesign.features.home.room.list
import android.content.Context import android.content.Context
import com.airbnb.epoxy.Typed2EpoxyController import com.airbnb.epoxy.Typed2EpoxyController
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R import im.vector.riotredesign.core.extensions.avatarDrawable
import im.vector.riotredesign.features.home.RoomSummaryViewHelper
class RoomSummaryController(private val context: Context, class RoomSummaryController(private val context: Context,
private val callback: Callback? = null private val callback: Callback? = null
@ -53,10 +52,9 @@ class RoomSummaryController(private val context: Context,
private fun buildRoomModels(summaries: List<RoomSummary>, selected: RoomSummary?) { private fun buildRoomModels(summaries: List<RoomSummary>, selected: RoomSummary?) {
summaries.forEach { summaries.forEach {
val roomSummaryViewHelper = RoomSummaryViewHelper(it)
RoomSummaryItem( RoomSummaryItem(
title = it.displayName, title = it.displayName,
avatarDrawable = roomSummaryViewHelper.avatarDrawable(context), avatarDrawable = context.avatarDrawable(it.displayName),
isSelected = it.roomId == selected?.roomId, isSelected = it.roomId == selected?.roomId,
listener = { callback?.onRoomSelected(it) } listener = { callback?.onRoomSelected(it) }
) )

View File

@ -16,7 +16,7 @@ data class RoomSummaryItem(
) : KotlinModel(R.layout.item_room) { ) : KotlinModel(R.layout.item_room) {
private val titleView by bind<TextView>(R.id.titleView) private val titleView by bind<TextView>(R.id.titleView)
private val avatarImageView by bind<ImageView>(R.id.avatarImageView) private val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
private val rootView by bind<CheckableFrameLayout>(R.id.itemRoomLayout) private val rootView by bind<CheckableFrameLayout>(R.id.itemRoomLayout)
override fun bind() { override fun bind() {

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="80dp"
android:padding="16dp">
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:paddingLeft="16dp"
android:paddingRight="16dp">
<ImageView
android:id="@+id/messageAvatarImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/messageMemberNameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="64dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingBottom="8dp"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/toolbarSubtitleView"
app:layout_constraintEnd_toStartOf="@+id/messageTimeView"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/messageTimeView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/brown_grey"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toTopOf="@id/messageMemberNameView"
tools:text="@tools:sample/date/hhmm" />
<TextView
android:id="@+id/messageTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="64dp"
android:layout_marginBottom="8dp"
android:textColor="@color/dark_grey"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView"
tools:text="Alright finished work, heading there in about 20 mins…Ping me when youre outside" />
</android.support.constraint.ConstraintLayout>

View File

@ -21,7 +21,7 @@
android:minHeight="48dp"> android:minHeight="48dp">
<ImageView <ImageView
android:id="@+id/avatarImageView" android:id="@+id/messageAvatarImageView"
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
@ -40,7 +40,7 @@
android:textSize="14sp" android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/avatarImageView" app:layout_constraintStart_toEndOf="@id/messageAvatarImageView"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" /> tools:text="@tools:sample/full_names" />

View File

@ -15,4 +15,5 @@
<color name="light_grey_blue">#9fa9ba</color> <color name="light_grey_blue">#9fa9ba</color>
<color name="cool_grey">#a5aab2</color> <color name="cool_grey">#a5aab2</color>
<color name="pale_grey_two">#ebedf8</color> <color name="pale_grey_two">#ebedf8</color>
<color name="brown_grey">#a5a5a5</color>
</resources> </resources>

View File

@ -1,5 +1,7 @@
package im.vector.matrix.android.api.session.events.model package im.vector.matrix.android.api.session.events.model
import im.vector.matrix.android.api.session.room.model.RoomMember
data class EnrichedEvent(val root: Event) { data class EnrichedEvent(val root: Event) {
val metadata = HashMap<String, Any>() val metadata = HashMap<String, Any>()
@ -31,3 +33,7 @@ data class EnrichedEvent(val root: Event) {
} }
} }
fun EnrichedEvent.roomMember(): RoomMember? {
return getMetadata<Event>(EventType.STATE_ROOM_MEMBER)?.content<RoomMember>()
}

View File

@ -1,37 +0,0 @@
package im.vector.matrix.android.internal.session.events.interceptor
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.interceptor.EnrichedEventInterceptor
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.database.query.where
import io.realm.Sort
import java.util.*
class IsLastEventInterceptor(val monarchy: Monarchy) : EnrichedEventInterceptor {
override fun canEnrich(event: EnrichedEvent): Boolean {
return true
}
override fun enrich(roomId: String, event: EnrichedEvent) {
monarchy.doWithRealm { realm ->
if (event.root.eventId == null) {
return@doWithRealm
}
val eventEntity = EventEntity.where(realm, event.root.eventId).findFirst()
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(event.root.eventId)).firstOrNull()
if (eventEntity == null || chunkEntity == null) {
return@doWithRealm
}
val sortedChunkEvents = chunkEntity.events.where().sort(EventEntityFields.ORIGIN_SERVER_TS, Sort.ASCENDING).findAll()
val isLastEvent = chunkEntity.prevToken == null && sortedChunkEvents?.indexOf(eventEntity) == 0
event.enrichWith(EnrichedEvent.IS_LAST_EVENT, isLastEvent)
}
}
}

View File

@ -11,7 +11,6 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.events.interceptor.IsLastEventInterceptor
import im.vector.matrix.android.internal.session.events.interceptor.MessageEventInterceptor import im.vector.matrix.android.internal.session.events.interceptor.MessageEventInterceptor
import io.realm.Sort import io.realm.Sort
@ -24,7 +23,6 @@ class DefaultTimelineHolder(private val roomId: String,
init { init {
eventInterceptors.add(MessageEventInterceptor(monarchy)) eventInterceptors.add(MessageEventInterceptor(monarchy))
eventInterceptors.add(IsLastEventInterceptor(monarchy))
} }
override fun liveTimeline(): LiveData<PagedList<EnrichedEvent>> { override fun liveTimeline(): LiveData<PagedList<EnrichedEvent>> {