Display most recent activity in room list (#220)
* Create `RoomLastMessageFormatter` to produce readable room summaries. * Add unit tests using Robolectric, fix bugs * Add changelog * Move `RoomLastMessageFormatter` back to `impl` module, allow it to receive an `EventTimelineItem` instead of `MessageContent`.
This commit is contained in:
parent
2c478f0e49
commit
d9183e4092
24 changed files with 1311 additions and 48 deletions
|
|
@ -16,6 +16,6 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.api
|
||||
|
||||
interface LastMessageFormatter {
|
||||
interface LastMessageTimestampFormatter {
|
||||
fun format(timestamp: Long?): String
|
||||
}
|
||||
|
|
@ -17,15 +17,15 @@
|
|||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageFormatter
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLastMessageFormatter @Inject constructor(
|
||||
class DefaultLastMessageTimestampFormatter @Inject constructor(
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
) : LastMessageFormatter {
|
||||
) : LastMessageTimestampFormatter {
|
||||
|
||||
override fun format(timestamp: Long?): String {
|
||||
if (timestamp == null) return ""
|
||||
|
|
@ -17,14 +17,14 @@
|
|||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageFormatter
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeClock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.TimeZone
|
||||
import org.junit.Test
|
||||
import java.util.Locale
|
||||
|
||||
class DefaultLastMessageFormatterTest {
|
||||
class DefaultLastMessageTimestampFormatterTest {
|
||||
|
||||
@Test
|
||||
fun `test null`() {
|
||||
|
|
@ -100,10 +100,10 @@ class DefaultLastMessageFormatterTest {
|
|||
/**
|
||||
* Create DefaultLastMessageFormatter and set current time to the provided date.
|
||||
*/
|
||||
private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): LastMessageFormatter {
|
||||
private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): LastMessageTimestampFormatter {
|
||||
val clock = FakeClock().also { it.givenInstant(Instant.parse(currentDate)) }
|
||||
val localDateTimeProvider = LocalDateTimeProvider(clock, TimeZone.UTC)
|
||||
val dateFormatters = DateFormatters(Locale.US, clock, TimeZone.UTC)
|
||||
return DefaultLastMessageFormatter(localDateTimeProvider, dateFormatters)
|
||||
return DefaultLastMessageTimestampFormatter(localDateTimeProvider, dateFormatters)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.test
|
||||
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageFormatter
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
|
||||
class FakeLastMessageFormatter : LastMessageFormatter {
|
||||
class FakeLastMessageTimestampFormatter : LastMessageTimestampFormatter {
|
||||
private var format = ""
|
||||
fun givenFormat(format: String) {
|
||||
this.format = format
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.matrix.api.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
|
||||
sealed interface RoomSummary {
|
||||
data class Empty(val identifier: String) : RoomSummary
|
||||
|
|
@ -35,7 +36,7 @@ data class RoomSummaryDetails(
|
|||
val name: String,
|
||||
val isDirect: Boolean,
|
||||
val avatarURLString: String?,
|
||||
val lastMessage: CharSequence?,
|
||||
val lastMessage: RoomMessage?,
|
||||
val lastMessageTimestamp: Long?,
|
||||
val unreadNotificationCount: Int,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ package io.element.android.libraries.matrix.api.room.message
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
|
||||
data class RoomMessage(
|
||||
val eventId: EventId,
|
||||
val body: String,
|
||||
val event: EventTimelineItem,
|
||||
val sender: UserId,
|
||||
val originServerTs: Long,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
|
||||
|
|
@ -28,19 +29,16 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
|
|||
val latestRoomMessage = slidingSyncRoom.latestRoomMessage()?.use {
|
||||
roomMessageFactory.create(it)
|
||||
}
|
||||
val computedLastMessage = when {
|
||||
latestRoomMessage == null -> null
|
||||
slidingSyncRoom.isDm() == true -> latestRoomMessage.body
|
||||
else -> "${latestRoomMessage.sender.value}: ${latestRoomMessage.body}"
|
||||
}
|
||||
return RoomSummaryDetails(
|
||||
roomId = RoomId(slidingSyncRoom.roomId()),
|
||||
name = slidingSyncRoom.name() ?: slidingSyncRoom.roomId(),
|
||||
isDirect = slidingSyncRoom.isDm() ?: false,
|
||||
avatarURLString = room?.avatarUrl(),
|
||||
unreadNotificationCount = slidingSyncRoom.unreadNotifications().use { it.notificationCount().toInt() },
|
||||
lastMessage = computedLastMessage,
|
||||
lastMessage = latestRoomMessage,
|
||||
lastMessageTimestamp = latestRoomMessage?.originServerTs
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,19 +16,19 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.room.message
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
|
||||
|
||||
class RoomMessageFactory {
|
||||
fun create(eventTimelineItem: EventTimelineItem?): RoomMessage? {
|
||||
fun create(eventTimelineItem: RustEventTimelineItem?): RoomMessage? {
|
||||
eventTimelineItem ?: return null
|
||||
val mappedTimelineItem = EventTimelineItemMapper().map(eventTimelineItem)
|
||||
return RoomMessage(
|
||||
eventId = EventId(eventTimelineItem.eventId() ?: ""),
|
||||
body = eventTimelineItem.content().asMessage()?.body() ?: "",
|
||||
sender = UserId(eventTimelineItem.sender()),
|
||||
originServerTs = eventTimelineItem.timestamp().toLong()
|
||||
eventId = mappedTimelineItem.eventId!!,
|
||||
event = mappedTimelineItem,
|
||||
sender = mappedTimelineItem.sender,
|
||||
originServerTs = mappedTimelineItem.timestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ val A_SPACE_ID = SpaceId("!aSpaceId")
|
|||
val A_ROOM_ID = RoomId("!aRoomId")
|
||||
val A_THREAD_ID = ThreadId("\$aThreadId")
|
||||
val AN_EVENT_ID = EventId("\$anEventId")
|
||||
const val A_UNIQUE_ID = "aUniqueId"
|
||||
|
||||
const val A_ROOM_NAME = "A room name"
|
||||
const val A_MESSAGE = "Hello world!"
|
||||
|
|
|
|||
|
|
@ -16,19 +16,33 @@
|
|||
|
||||
package io.element.android.libraries.matrix.test.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomSummary
|
||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
|
||||
fun aRoomSummaryFilled(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
name: String = A_ROOM_NAME,
|
||||
isDirect: Boolean = false,
|
||||
avatarURLString: String? = null,
|
||||
lastMessage: CharSequence? = A_MESSAGE,
|
||||
lastMessage: RoomMessage? = aRoomMessage(),
|
||||
lastMessageTimestamp: Long? = null,
|
||||
unreadNotificationCount: Int = 2,
|
||||
) = RoomSummary.Filled(
|
||||
|
|
@ -48,7 +62,7 @@ fun aRoomSummaryDetail(
|
|||
name: String = A_ROOM_NAME,
|
||||
isDirect: Boolean = false,
|
||||
avatarURLString: String? = null,
|
||||
lastMessage: CharSequence? = A_MESSAGE,
|
||||
lastMessage: RoomMessage? = aRoomMessage(),
|
||||
lastMessageTimestamp: Long? = null,
|
||||
unreadNotificationCount: Int = 2,
|
||||
) = RoomSummaryDetails(
|
||||
|
|
@ -60,3 +74,65 @@ fun aRoomSummaryDetail(
|
|||
lastMessageTimestamp = lastMessageTimestamp,
|
||||
unreadNotificationCount = unreadNotificationCount,
|
||||
)
|
||||
|
||||
fun aRoomMessage(
|
||||
eventId: EventId = AN_EVENT_ID,
|
||||
event: EventTimelineItem = anEventTimelineItem(),
|
||||
userId: UserId = A_USER_ID,
|
||||
timestamp: Long = 0L,
|
||||
) = RoomMessage(
|
||||
eventId = eventId,
|
||||
event = event,
|
||||
sender = userId,
|
||||
originServerTs = timestamp,
|
||||
)
|
||||
|
||||
fun anEventTimelineItem(
|
||||
uniqueIdentifier: String = A_UNIQUE_ID,
|
||||
eventId: EventId = AN_EVENT_ID,
|
||||
isEditable: Boolean = false,
|
||||
isLocal: Boolean = false,
|
||||
isOwn: Boolean = false,
|
||||
isRemote: Boolean = false,
|
||||
localSendState: EventSendState? = null,
|
||||
reactions: List<EventReaction> = emptyList(),
|
||||
sender: UserId = A_USER_ID,
|
||||
senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(),
|
||||
timestamp: Long = 0L,
|
||||
content: EventContent = aProfileChangeMessageContent(),
|
||||
) = EventTimelineItem(
|
||||
uniqueIdentifier = uniqueIdentifier,
|
||||
eventId = eventId,
|
||||
isEditable = isEditable,
|
||||
isLocal = isLocal,
|
||||
isOwn = isOwn,
|
||||
isRemote = isRemote,
|
||||
localSendState = localSendState,
|
||||
reactions = reactions,
|
||||
sender = sender,
|
||||
senderProfile = senderProfile,
|
||||
timestamp = timestamp,
|
||||
content = content,
|
||||
)
|
||||
|
||||
fun aProfileTimelineDetails(
|
||||
displayName: String? = A_USER_NAME,
|
||||
displayNameAmbiguous: Boolean = false,
|
||||
avatarUrl: String? = null
|
||||
): ProfileTimelineDetails = ProfileTimelineDetails.Ready(
|
||||
displayName = displayName,
|
||||
displayNameAmbiguous = displayNameAmbiguous,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
||||
fun aProfileChangeMessageContent(
|
||||
displayName: String? = null,
|
||||
prevDisplayName: String? = null,
|
||||
avatarUrl: String? = null,
|
||||
prevAvatarUrl: String? = null,
|
||||
) = ProfileChangeContent(
|
||||
displayName = displayName,
|
||||
prevDisplayName = prevDisplayName,
|
||||
avatarUrl = avatarUrl,
|
||||
prevAvatarUrl = prevAvatarUrl,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,21 @@
|
|||
<!-- Create room -->
|
||||
<string name="search_for_someone">Search for someone</string>
|
||||
<string name="new_room">New room</string>
|
||||
<!-- Room list -->
|
||||
<string name="notice_room_invite_accepted_by_you">You accepted the invite</string>
|
||||
<string name="notice_room_invite_accepted">%1$s accepted the invite</string>
|
||||
<string name="notice_room_knock">%1$s requested to join</string>
|
||||
<string name="notice_room_knock_by_you">You requested to join</string>
|
||||
<string name="notice_room_knock_accepted">%1$s allowed %2$s to join</string>
|
||||
<string name="notice_room_knock_accepted_by_you">%1$s allowed you to join</string>
|
||||
<string name="notice_room_knock_retracted">%1$s is no longer interested in joining</string>
|
||||
<string name="notice_room_knock_retracted_by_you">You cancelled your request to join</string>
|
||||
<string name="notice_room_knock_denied">%1$s rejected %2$s\'s request to join</string>
|
||||
<string name="notice_room_knock_denied_by_you">You rejected %1$s\'s request to join</string>
|
||||
<string name="notice_room_knock_denied_you">%1$s rejected your request to join</string>
|
||||
<string name="notice_room_unknown_membership_change">%1$s made an unknown change to their membership</string>
|
||||
<string name="room_timeline_item_unsupported">Unsupported event</string>
|
||||
<string name="unknown_message_content_type_error">Event type not handled by EAX</string>
|
||||
|
||||
<string name="verification_title_initial">Open an existing session</string>
|
||||
<string name="verification_title_waiting">Waiting to accept request</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue