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
1
changelog.d/217.bugfix
Normal file
1
changelog.d/217.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Display most recent activity for each room in room list
|
||||||
|
|
@ -25,6 +25,12 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.roomlist.impl"
|
namespace = "io.element.android.features.roomlist.impl"
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -52,6 +58,7 @@ dependencies {
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.dateformatter.test)
|
testImplementation(projects.libraries.dateformatter.test)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,330 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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 io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||||
|
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.api.timeline.item.event.RedactedContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||||
|
// TODO replace with StringProvider
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
|
) : RoomLastMessageFormatter {
|
||||||
|
|
||||||
|
override fun processMessageItem(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? {
|
||||||
|
val isOutgoing = event.sender == matrixClient.sessionId
|
||||||
|
val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value
|
||||||
|
return when (val content = event.content) {
|
||||||
|
is MessageContent -> processMessageContents(content, senderDisplayName, isDmRoom)
|
||||||
|
RedactedContent -> {
|
||||||
|
val message = context.getString(StringR.string.event_redacted)
|
||||||
|
if (!isDmRoom) {
|
||||||
|
prefix(message, senderDisplayName)
|
||||||
|
} else {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is StickerContent -> {
|
||||||
|
content.body
|
||||||
|
}
|
||||||
|
is UnableToDecryptContent -> {
|
||||||
|
val message = context.getString(StringR.string.encryption_information_decryption_error)
|
||||||
|
if (!isDmRoom) {
|
||||||
|
prefix(message, senderDisplayName)
|
||||||
|
} else {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is RoomMembershipContent -> {
|
||||||
|
processRoomMembershipChange(content, senderDisplayName, isOutgoing)
|
||||||
|
}
|
||||||
|
is ProfileChangeContent -> {
|
||||||
|
processProfileChangeContent(content, senderDisplayName, isOutgoing)
|
||||||
|
}
|
||||||
|
is StateContent -> {
|
||||||
|
processRoomStateChange(content, senderDisplayName, isOutgoing)
|
||||||
|
}
|
||||||
|
is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> {
|
||||||
|
prefixIfNeeded(context.getString(StringR.string.room_timeline_item_unsupported), senderDisplayName, isDmRoom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processMessageContents(messageContent: MessageContent, senderDisplayName: String, isDmRoom: Boolean): CharSequence? {
|
||||||
|
val messageType: MessageType = messageContent.type ?: return null
|
||||||
|
|
||||||
|
val internalMessage = when (messageType) {
|
||||||
|
// Doesn't need a prefix
|
||||||
|
is EmoteMessageType -> {
|
||||||
|
return "- $senderDisplayName ${messageType.body}"
|
||||||
|
}
|
||||||
|
is TextMessageType -> {
|
||||||
|
messageType.body
|
||||||
|
}
|
||||||
|
is VideoMessageType -> {
|
||||||
|
context.getString(StringR.string.sent_a_video)
|
||||||
|
}
|
||||||
|
is ImageMessageType -> {
|
||||||
|
context.getString(StringR.string.sent_an_image)
|
||||||
|
}
|
||||||
|
is FileMessageType -> {
|
||||||
|
context.getString(StringR.string.sent_a_file)
|
||||||
|
}
|
||||||
|
is AudioMessageType -> {
|
||||||
|
context.getString(StringR.string.sent_an_audio_file)
|
||||||
|
}
|
||||||
|
UnknownMessageType -> {
|
||||||
|
context.getString(StringR.string.unknown_message_content_type_error)
|
||||||
|
}
|
||||||
|
is NoticeMessageType -> {
|
||||||
|
messageType.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefixIfNeeded(internalMessage, senderDisplayName, isDmRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processRoomMembershipChange(membershipContent: RoomMembershipContent, senderDisplayName: String, senderIsYou: Boolean): CharSequence? {
|
||||||
|
val userId = membershipContent.userId
|
||||||
|
val memberIsYou = userId == matrixClient.sessionId
|
||||||
|
return when (val change = membershipContent.change) {
|
||||||
|
MembershipChange.JOINED -> if (memberIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_join_by_you)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_join, userId.value)
|
||||||
|
}
|
||||||
|
MembershipChange.LEFT -> if (memberIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_leave_by_you)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_leave, userId.value)
|
||||||
|
}
|
||||||
|
MembershipChange.BANNED, MembershipChange.KICKED_AND_BANNED -> if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_ban_by_you, userId.value)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_ban, senderDisplayName, userId.value)
|
||||||
|
}
|
||||||
|
MembershipChange.UNBANNED -> if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_unban_by_you, userId.value)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_unban, senderDisplayName, userId.value)
|
||||||
|
}
|
||||||
|
MembershipChange.KICKED -> if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_remove_by_you, userId.value)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_remove, senderDisplayName, userId.value)
|
||||||
|
}
|
||||||
|
MembershipChange.INVITED -> if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_invite_by_you, userId.value)
|
||||||
|
} else if (memberIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_invite_you, senderDisplayName)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_invite, senderDisplayName, userId.value)
|
||||||
|
}
|
||||||
|
MembershipChange.INVITATION_ACCEPTED -> if (memberIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_invite_accepted_by_you)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_invite_accepted, userId.value)
|
||||||
|
}
|
||||||
|
MembershipChange.INVITATION_REJECTED -> if (memberIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_reject_by_you)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_reject, userId.value)
|
||||||
|
}
|
||||||
|
MembershipChange.INVITATION_REVOKED -> if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_third_party_revoked_invite_by_you, userId.value)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_third_party_revoked_invite, senderDisplayName, userId.value)
|
||||||
|
}
|
||||||
|
MembershipChange.KNOCKED -> if (memberIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_knock_by_you)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_knock, userId.value)
|
||||||
|
}
|
||||||
|
MembershipChange.KNOCK_ACCEPTED -> if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_knock_accepted_by_you, userId.value)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_knock_accepted, senderDisplayName, userId.value)
|
||||||
|
}
|
||||||
|
MembershipChange.KNOCK_RETRACTED -> if (memberIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_knock_retracted_by_you)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_knock_retracted, userId.value)
|
||||||
|
}
|
||||||
|
MembershipChange.KNOCK_DENIED -> if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_knock_denied_by_you, userId.value)
|
||||||
|
} else if (memberIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_knock_denied_you, senderDisplayName)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_knock_denied, senderDisplayName, userId.value)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Timber.v("Filtering timeline item for room membership: $membershipContent")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processRoomStateChange(stateContent: StateContent, senderDisplayName: String, senderIsYou: Boolean): CharSequence? {
|
||||||
|
return when (val content = stateContent.content) {
|
||||||
|
is OtherState.RoomAvatar -> {
|
||||||
|
val hasAvatarUrl = content.url != null
|
||||||
|
when {
|
||||||
|
senderIsYou && hasAvatarUrl -> context.getString(StringR.string.notice_room_avatar_changed_by_you)
|
||||||
|
senderIsYou && !hasAvatarUrl -> context.getString(StringR.string.notice_room_avatar_removed_by_you)
|
||||||
|
!senderIsYou && hasAvatarUrl -> context.getString(StringR.string.notice_room_avatar_changed, senderDisplayName)
|
||||||
|
else -> context.getString(StringR.string.notice_room_avatar_removed, senderDisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is OtherState.RoomCreate -> {
|
||||||
|
if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_created_by_you)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_created, senderDisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is OtherState.RoomEncryption -> context.getString(StringR.string.encryption_enabled)
|
||||||
|
is OtherState.RoomName -> {
|
||||||
|
val hasRoomName = content.name != null
|
||||||
|
when {
|
||||||
|
senderIsYou && hasRoomName -> context.getString(StringR.string.notice_room_name_changed_by_you, content.name)
|
||||||
|
senderIsYou && !hasRoomName -> context.getString(StringR.string.notice_room_name_removed_by_you)
|
||||||
|
!senderIsYou && hasRoomName -> context.getString(StringR.string.notice_room_name_changed, senderDisplayName, content.name)
|
||||||
|
else -> context.getString(StringR.string.notice_room_name_removed, senderDisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is OtherState.RoomThirdPartyInvite -> {
|
||||||
|
if (content.displayName == null) {
|
||||||
|
Timber.e("RoomThirdPartyInvite undisplayable due to missing name")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_room_third_party_invite_by_you, content.displayName)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_room_third_party_invite, senderDisplayName, content.displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is OtherState.RoomTopic -> {
|
||||||
|
val hasRoomTopic = content.topic != null
|
||||||
|
when {
|
||||||
|
senderIsYou && hasRoomTopic -> context.getString(StringR.string.notice_room_topic_changed_by_you, content.topic)
|
||||||
|
senderIsYou && !hasRoomTopic -> context.getString(StringR.string.notice_room_topic_removed_by_you)
|
||||||
|
!senderIsYou && hasRoomTopic -> context.getString(StringR.string.notice_room_topic_changed, senderDisplayName, content.topic)
|
||||||
|
else -> context.getString(StringR.string.notice_room_topic_removed, senderDisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Timber.v("Filtering timeline item for room state change: $content")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processProfileChangeContent(
|
||||||
|
profileChangeContent: ProfileChangeContent,
|
||||||
|
senderDisplayName: String,
|
||||||
|
senderIsYou: Boolean
|
||||||
|
): String? = profileChangeContent.run {
|
||||||
|
val displayNameChanged = displayName != prevDisplayName
|
||||||
|
val avatarChanged = avatarUrl != prevAvatarUrl
|
||||||
|
return when {
|
||||||
|
avatarChanged && displayNameChanged -> {
|
||||||
|
val message = processProfileChangeContent(profileChangeContent.copy(avatarUrl = null, prevAvatarUrl = null), senderDisplayName, senderIsYou)
|
||||||
|
val avatarChangedToo = context.getString(StringR.string.notice_avatar_changed_too)
|
||||||
|
"$message\n$avatarChangedToo"
|
||||||
|
}
|
||||||
|
displayNameChanged -> {
|
||||||
|
if (displayName != null && prevDisplayName != null) {
|
||||||
|
if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_display_name_changed_from_by_you, prevDisplayName, displayName)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_display_name_changed_from, senderDisplayName, prevDisplayName, displayName)
|
||||||
|
}
|
||||||
|
} else if (displayName != null) {
|
||||||
|
if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_display_name_set_by_you, displayName)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_display_name_set, senderDisplayName, displayName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_display_name_removed_by_you, prevDisplayName)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_display_name_removed, senderDisplayName, prevDisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avatarChanged -> {
|
||||||
|
if (senderIsYou) {
|
||||||
|
context.getString(StringR.string.notice_avatar_url_changed_by_you)
|
||||||
|
} else {
|
||||||
|
context.getString(StringR.string.notice_avatar_url_changed, senderDisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prefixIfNeeded(message: String, senderDisplayName: String, isDmRoom: Boolean): CharSequence = if (isDmRoom) {
|
||||||
|
message
|
||||||
|
} else {
|
||||||
|
prefix(message, senderDisplayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prefix(message: String, senderDisplayName: String): AnnotatedString {
|
||||||
|
return buildAnnotatedString {
|
||||||
|
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||||
|
append(senderDisplayName)
|
||||||
|
}
|
||||||
|
append(": ")
|
||||||
|
append(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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 io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
|
|
||||||
|
interface RoomLastMessageFormatter {
|
||||||
|
fun processMessageItem(event: EventTimelineItem, isDmRoom: Boolean): CharSequence?
|
||||||
|
}
|
||||||
|
|
@ -30,15 +30,16 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.parallelMap
|
import io.element.android.libraries.core.coroutine.parallelMap
|
||||||
import io.element.android.libraries.dateformatter.api.LastMessageFormatter
|
import io.element.android.libraries.core.extensions.orEmpty
|
||||||
|
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
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.RoomSummary
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -52,7 +53,8 @@ private const val extendedRangeSize = 40
|
||||||
|
|
||||||
class RoomListPresenter @Inject constructor(
|
class RoomListPresenter @Inject constructor(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val lastMessageFormatter: LastMessageFormatter,
|
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
|
||||||
|
private val roomLastMessageFormatter: RoomLastMessageFormatter,
|
||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
) : Presenter<RoomListState> {
|
) : Presenter<RoomListState> {
|
||||||
|
|
||||||
|
|
@ -169,8 +171,10 @@ class RoomListPresenter @Inject constructor(
|
||||||
id = roomSummary.identifier(),
|
id = roomSummary.identifier(),
|
||||||
name = roomSummary.details.name,
|
name = roomSummary.details.name,
|
||||||
hasUnread = roomSummary.details.unreadNotificationCount > 0,
|
hasUnread = roomSummary.details.unreadNotificationCount > 0,
|
||||||
timestamp = lastMessageFormatter.format(roomSummary.details.lastMessageTimestamp),
|
timestamp = lastMessageTimestampFormatter.format(roomSummary.details.lastMessageTimestamp),
|
||||||
lastMessage = roomSummary.details.lastMessage,
|
lastMessage = roomSummary.details.lastMessage?.let { message ->
|
||||||
|
roomLastMessageFormatter.processMessageItem(message.event, roomSummary.details.isDirect)
|
||||||
|
}.orEmpty(),
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,6 @@ import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import io.element.android.libraries.designsystem.R as DrawableR
|
import io.element.android.libraries.designsystem.R as DrawableR
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Outline
|
import androidx.compose.ui.graphics.Outline
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -55,6 +56,7 @@ import androidx.compose.ui.unit.sp
|
||||||
import com.google.accompanist.placeholder.material.placeholder
|
import com.google.accompanist.placeholder.material.placeholder
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider
|
||||||
|
import io.element.android.libraries.core.extensions.orEmpty
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
@ -133,13 +135,15 @@ internal fun DefaultRoomSummaryRow(
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
// Last Message
|
// Last Message
|
||||||
|
val attributedLastMessage = (room.lastMessage as? AnnotatedString)
|
||||||
|
?: AnnotatedString(room.lastMessage.orEmpty().toString())
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.placeholder(
|
modifier = Modifier.placeholder(
|
||||||
visible = room.isPlaceholder,
|
visible = room.isPlaceholder,
|
||||||
shape = TextPlaceholderShape,
|
shape = TextPlaceholderShape,
|
||||||
color = ElementTheme.colors.roomListPlaceHolder(),
|
color = ElementTheme.colors.roomListPlaceHolder(),
|
||||||
),
|
),
|
||||||
text = room.lastMessage?.toString().orEmpty(),
|
text = attributedLastMessage,
|
||||||
color = MaterialTheme.roomListRoomMessage(),
|
color = MaterialTheme.roomListRoomMessage(),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,757 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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 io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.room.aProfileChangeMessageContent
|
||||||
|
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.RuntimeEnvironment
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
class DefaultRoomLastMessageFormatterTests {
|
||||||
|
|
||||||
|
private lateinit var context: Context
|
||||||
|
private lateinit var fakeMatrixClient: FakeMatrixClient
|
||||||
|
private lateinit var formatter: DefaultRoomLastMessageFormatter
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
context = RuntimeEnvironment.getApplication() as Context
|
||||||
|
fakeMatrixClient = FakeMatrixClient()
|
||||||
|
formatter = DefaultRoomLastMessageFormatter(context, fakeMatrixClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Redacted content`() {
|
||||||
|
val expected = "Message removed"
|
||||||
|
val senderName = "Someone"
|
||||||
|
sequenceOf(false, true).forEach { isDm ->
|
||||||
|
val message = createRoomEvent(false, senderName, RedactedContent)
|
||||||
|
val result = formatter.processMessageItem(message, isDm)
|
||||||
|
if (isDm) {
|
||||||
|
Truth.assertThat(result).isEqualTo(expected)
|
||||||
|
} else {
|
||||||
|
Truth.assertThat(result).isInstanceOf(AnnotatedString::class.java)
|
||||||
|
Truth.assertThat(result.toString()).isEqualTo("$senderName: $expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Sticker content`() {
|
||||||
|
val body = "body"
|
||||||
|
val info = ImageInfo(null, null, null, null, null, null, null)
|
||||||
|
val message = createRoomEvent(false, null, StickerContent(body, info, "url"))
|
||||||
|
val result = formatter.processMessageItem(message, false)
|
||||||
|
Truth.assertThat(result).isEqualTo(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Unable to decrypt content`() {
|
||||||
|
val expected = "Decryption error"
|
||||||
|
val senderName = "Someone"
|
||||||
|
sequenceOf(false, true).forEach { isDm ->
|
||||||
|
val message = createRoomEvent(false, senderName, UnableToDecryptContent(UnableToDecryptContent.Data.Unknown))
|
||||||
|
val result = formatter.processMessageItem(message, isDm)
|
||||||
|
if (isDm) {
|
||||||
|
Truth.assertThat(result).isEqualTo(expected)
|
||||||
|
} else {
|
||||||
|
Truth.assertThat(result).isInstanceOf(AnnotatedString::class.java)
|
||||||
|
Truth.assertThat(result.toString()).isEqualTo("$senderName: $expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `FailedToParseMessageLike, FailedToParseState & Unknown content`() {
|
||||||
|
val expected = "Unsupported event"
|
||||||
|
val senderName = "Someone"
|
||||||
|
sequenceOf(false, true).forEach { isDm ->
|
||||||
|
sequenceOf(
|
||||||
|
FailedToParseMessageLikeContent("", ""),
|
||||||
|
FailedToParseStateContent("", "", ""),
|
||||||
|
UnknownContent,
|
||||||
|
).forEach { type ->
|
||||||
|
val message = createRoomEvent(false, senderName, type)
|
||||||
|
val result = formatter.processMessageItem(message, isDm)
|
||||||
|
if (isDm) {
|
||||||
|
Truth.assertWithMessage("$type was not properly handled").that(result).isEqualTo(expected)
|
||||||
|
} else {
|
||||||
|
Truth.assertWithMessage("$type does not create an AnnotatedString").that(result).isInstanceOf(AnnotatedString::class.java)
|
||||||
|
Truth.assertWithMessage("$type was not properly handled").that(result.toString()).isEqualTo("$senderName: $expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Message contents
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Message contents`() {
|
||||||
|
val body = "Shared body"
|
||||||
|
fun createMessageContent(type: MessageType): MessageContent {
|
||||||
|
return MessageContent(body, null, false, type)
|
||||||
|
}
|
||||||
|
val sharedContentMessagesTypes = arrayOf(
|
||||||
|
TextMessageType(body, null),
|
||||||
|
VideoMessageType(body, "url", null),
|
||||||
|
AudioMessageType(body, "url", null),
|
||||||
|
ImageMessageType(body, "url", null),
|
||||||
|
FileMessageType(body, "url", null),
|
||||||
|
NoticeMessageType(body, null),
|
||||||
|
EmoteMessageType(body, null),
|
||||||
|
)
|
||||||
|
val senderName = "Someone"
|
||||||
|
val resultsInRoom = mutableListOf<Pair<MessageType, CharSequence?>>()
|
||||||
|
val resultsInDm = mutableListOf<Pair<MessageType, CharSequence?>>()
|
||||||
|
|
||||||
|
// Create messages for all types in DM and Room mode
|
||||||
|
sequenceOf(false, true).forEach { isDm ->
|
||||||
|
sharedContentMessagesTypes.forEach { type ->
|
||||||
|
val content = createMessageContent(type)
|
||||||
|
val message = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content)
|
||||||
|
val result = formatter.processMessageItem(message, isDmRoom = isDm)
|
||||||
|
if (isDm) {
|
||||||
|
resultsInDm.add(type to result)
|
||||||
|
} else {
|
||||||
|
resultsInRoom.add(type to result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val unknownMessage = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = createMessageContent(UnknownMessageType))
|
||||||
|
val result = UnknownMessageType to formatter.processMessageItem(unknownMessage, isDmRoom = isDm)
|
||||||
|
if (isDm) {
|
||||||
|
resultsInDm.add(result)
|
||||||
|
} else {
|
||||||
|
resultsInRoom.add(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify results of DM mode
|
||||||
|
for ((type, result) in resultsInDm) {
|
||||||
|
val expectedResult = when (type) {
|
||||||
|
is VideoMessageType -> "Video."
|
||||||
|
is AudioMessageType -> "Audio"
|
||||||
|
is ImageMessageType -> "Image."
|
||||||
|
is FileMessageType -> "File"
|
||||||
|
is EmoteMessageType -> "- $senderName ${type.body}"
|
||||||
|
is TextMessageType, is NoticeMessageType -> body
|
||||||
|
UnknownMessageType -> "Event type not handled by EAX"
|
||||||
|
}
|
||||||
|
Truth.assertWithMessage("$type was not properly handled").that(result).isEqualTo(expectedResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify results of Room mode
|
||||||
|
for ((type, result) in resultsInRoom) {
|
||||||
|
val string = result.toString()
|
||||||
|
val expectedResult = when (type) {
|
||||||
|
is VideoMessageType -> "$senderName: Video."
|
||||||
|
is AudioMessageType -> "$senderName: Audio"
|
||||||
|
is ImageMessageType -> "$senderName: Image."
|
||||||
|
is FileMessageType -> "$senderName: File"
|
||||||
|
is EmoteMessageType -> "- $senderName ${type.body}"
|
||||||
|
is TextMessageType, is NoticeMessageType -> "$senderName: $body"
|
||||||
|
UnknownMessageType -> "$senderName: Event type not handled by EAX"
|
||||||
|
}
|
||||||
|
val shouldCreateAnnotatedString = when (type) {
|
||||||
|
is VideoMessageType -> true
|
||||||
|
is AudioMessageType -> true
|
||||||
|
is ImageMessageType -> true
|
||||||
|
is FileMessageType -> true
|
||||||
|
is EmoteMessageType -> false
|
||||||
|
is TextMessageType, is NoticeMessageType -> true
|
||||||
|
UnknownMessageType -> true
|
||||||
|
}
|
||||||
|
if (shouldCreateAnnotatedString) {
|
||||||
|
Truth.assertWithMessage("$type doesn't produce an AnnotatedString")
|
||||||
|
.that(result)
|
||||||
|
.isInstanceOf(AnnotatedString::class.java)
|
||||||
|
}
|
||||||
|
Truth.assertWithMessage("$type was not properly handled").that(string).isEqualTo(expectedResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Membership change
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - joined`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED)
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.JOINED)
|
||||||
|
|
||||||
|
val youJoinedRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
|
||||||
|
val youJoinedRoom = formatter.processMessageItem(youJoinedRoomEvent, false)
|
||||||
|
Truth.assertThat(youJoinedRoom).isEqualTo("You joined the room")
|
||||||
|
|
||||||
|
val someoneJoinedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneJoinedRoom = formatter.processMessageItem(someoneJoinedRoomEvent, false)
|
||||||
|
Truth.assertThat(someoneJoinedRoom).isEqualTo("${someoneContent.userId.value} joined the room")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - left`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.LEFT)
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.LEFT)
|
||||||
|
|
||||||
|
val youLeftRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
|
||||||
|
val youLeftRoom = formatter.processMessageItem(youLeftRoomEvent, false)
|
||||||
|
Truth.assertThat(youLeftRoom).isEqualTo("You left the room")
|
||||||
|
|
||||||
|
val someoneLeftRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneLeftRoom = formatter.processMessageItem(someoneLeftRoomEvent, false)
|
||||||
|
Truth.assertThat(someoneLeftRoom).isEqualTo("${someoneContent.userId.value} left the room")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - banned`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val youContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.BANNED)
|
||||||
|
val youKickedContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.KICKED_AND_BANNED)
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.BANNED)
|
||||||
|
val someoneKickedContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.KICKED_AND_BANNED)
|
||||||
|
|
||||||
|
val youBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
|
||||||
|
val youBanned = formatter.processMessageItem(youBannedEvent, false)
|
||||||
|
Truth.assertThat(youBanned).isEqualTo("You banned ${youContent.userId.value}")
|
||||||
|
|
||||||
|
val youKickBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youKickedContent)
|
||||||
|
val youKickedBanned = formatter.processMessageItem(youKickBannedEvent, false)
|
||||||
|
Truth.assertThat(youKickedBanned).isEqualTo("You banned ${youContent.userId.value}")
|
||||||
|
|
||||||
|
val someoneBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneBanned = formatter.processMessageItem(someoneBannedEvent, false)
|
||||||
|
Truth.assertThat(someoneBanned).isEqualTo("$otherName banned ${someoneContent.userId.value}")
|
||||||
|
|
||||||
|
val someoneKickBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneKickedContent)
|
||||||
|
val someoneKickBanned = formatter.processMessageItem(someoneKickBannedEvent, false)
|
||||||
|
Truth.assertThat(someoneKickBanned).isEqualTo("$otherName banned ${someoneContent.userId.value}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - unban`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val youContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.UNBANNED)
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.UNBANNED)
|
||||||
|
|
||||||
|
val youUnbannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
|
||||||
|
val youUnbanned = formatter.processMessageItem(youUnbannedEvent, false)
|
||||||
|
Truth.assertThat(youUnbanned).isEqualTo("You unbanned ${youContent.userId.value}")
|
||||||
|
|
||||||
|
val someoneUnbannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneUnbanned = formatter.processMessageItem(someoneUnbannedEvent, false)
|
||||||
|
Truth.assertThat(someoneUnbanned).isEqualTo("$otherName unbanned ${someoneContent.userId.value}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - kicked`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val youContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.KICKED)
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.KICKED)
|
||||||
|
|
||||||
|
val youKickedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
|
||||||
|
val youKicked = formatter.processMessageItem(youKickedEvent, false)
|
||||||
|
Truth.assertThat(youKicked).isEqualTo("You removed ${youContent.userId.value}")
|
||||||
|
|
||||||
|
val someoneKickedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneKicked = formatter.processMessageItem(someoneKickedEvent, false)
|
||||||
|
Truth.assertThat(someoneKicked).isEqualTo("$otherName removed ${someoneContent.userId.value}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - invited`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.INVITED)
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.INVITED)
|
||||||
|
|
||||||
|
val youWereInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent)
|
||||||
|
val youWereInvited = formatter.processMessageItem(youWereInvitedEvent, false)
|
||||||
|
Truth.assertThat(youWereInvited).isEqualTo("$otherName invited you")
|
||||||
|
|
||||||
|
val youInvitedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
|
||||||
|
val youInvited = formatter.processMessageItem(youInvitedEvent, false)
|
||||||
|
Truth.assertThat(youInvited).isEqualTo("You invited ${someoneContent.userId.value}")
|
||||||
|
|
||||||
|
val someoneInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneInvited = formatter.processMessageItem(someoneInvitedEvent, false)
|
||||||
|
Truth.assertThat(someoneInvited).isEqualTo("$otherName invited ${someoneContent.userId.value}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - invitation accepted`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.INVITATION_ACCEPTED)
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.INVITATION_ACCEPTED)
|
||||||
|
|
||||||
|
val youAcceptedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
|
||||||
|
val youAcceptedInvite = formatter.processMessageItem(youAcceptedInviteEvent, false)
|
||||||
|
Truth.assertThat(youAcceptedInvite).isEqualTo("You accepted the invite")
|
||||||
|
|
||||||
|
val someoneAcceptedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneAcceptedInvite = formatter.processMessageItem(someoneAcceptedInviteEvent, false)
|
||||||
|
Truth.assertThat(someoneAcceptedInvite).isEqualTo("${someoneContent.userId.value} accepted the invite")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - invitation rejected`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.INVITATION_REJECTED)
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.INVITATION_REJECTED)
|
||||||
|
|
||||||
|
val youRejectedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
|
||||||
|
val youRejectedInvite = formatter.processMessageItem(youRejectedInviteEvent, false)
|
||||||
|
Truth.assertThat(youRejectedInvite).isEqualTo("You rejected the invitation")
|
||||||
|
|
||||||
|
val someoneRejectedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneRejectedInvite = formatter.processMessageItem(someoneRejectedInviteEvent, false)
|
||||||
|
Truth.assertThat(someoneRejectedInvite).isEqualTo("${someoneContent.userId.value} rejected the invitation")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - invitation revoked`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.INVITATION_REVOKED)
|
||||||
|
|
||||||
|
val youRevokedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
|
||||||
|
val youRevokedInvite = formatter.processMessageItem(youRevokedInviteEvent, false)
|
||||||
|
Truth.assertThat(youRevokedInvite).isEqualTo("You revoked the invitation for ${someoneContent.userId.value} to join the room")
|
||||||
|
|
||||||
|
val someoneRevokedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneRevokedInvite = formatter.processMessageItem(someoneRevokedInviteEvent, false)
|
||||||
|
Truth.assertThat(someoneRevokedInvite).isEqualTo("$otherName revoked the invitation for ${someoneContent.userId.value} to join the room")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - knocked`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.KNOCKED)
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.KNOCKED)
|
||||||
|
|
||||||
|
val youKnockedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
|
||||||
|
val youKnocked = formatter.processMessageItem(youKnockedEvent, false)
|
||||||
|
Truth.assertThat(youKnocked).isEqualTo("You requested to join")
|
||||||
|
|
||||||
|
val someoneKnockedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneKnocked = formatter.processMessageItem(someoneKnockedEvent, false)
|
||||||
|
Truth.assertThat(someoneKnocked).isEqualTo("${someoneContent.userId.value} requested to join")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - knock accepted`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.KNOCK_ACCEPTED)
|
||||||
|
|
||||||
|
val youAcceptedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
|
||||||
|
val youAcceptedKnock = formatter.processMessageItem(youAcceptedKnockEvent, false)
|
||||||
|
Truth.assertThat(youAcceptedKnock).isEqualTo("${someoneContent.userId.value} allowed you to join")
|
||||||
|
|
||||||
|
val someoneAcceptedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneAcceptedKnock = formatter.processMessageItem(someoneAcceptedKnockEvent, false)
|
||||||
|
Truth.assertThat(someoneAcceptedKnock).isEqualTo("$otherName allowed ${someoneContent.userId.value} to join")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - knock retracted`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.KNOCK_RETRACTED)
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.KNOCK_RETRACTED)
|
||||||
|
|
||||||
|
val youRetractedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
|
||||||
|
val youRetractedKnock = formatter.processMessageItem(youRetractedKnockEvent, false)
|
||||||
|
Truth.assertThat(youRetractedKnock).isEqualTo("You cancelled your request to join")
|
||||||
|
|
||||||
|
val someoneRetractedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneRetractedKnock = formatter.processMessageItem(someoneRetractedKnockEvent, false)
|
||||||
|
Truth.assertThat(someoneRetractedKnock).isEqualTo("${someoneContent.userId.value} is no longer interested in joining")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - knock denied`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.KNOCK_DENIED)
|
||||||
|
val someoneContent = RoomMembershipContent(UserId("someone_else"), MembershipChange.KNOCK_DENIED)
|
||||||
|
|
||||||
|
val youDeniedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
|
||||||
|
val youDeniedKnock = formatter.processMessageItem(youDeniedKnockEvent, false)
|
||||||
|
Truth.assertThat(youDeniedKnock).isEqualTo("You rejected ${someoneContent.userId.value}'s request to join")
|
||||||
|
|
||||||
|
val someoneDeniedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||||
|
val someoneDeniedKnock = formatter.processMessageItem(someoneDeniedKnockEvent, false)
|
||||||
|
Truth.assertThat(someoneDeniedKnock).isEqualTo("$otherName rejected ${someoneContent.userId.value}'s request to join")
|
||||||
|
|
||||||
|
val someoneDeniedYourKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent)
|
||||||
|
val someoneDeniedYourKnock = formatter.processMessageItem(someoneDeniedYourKnockEvent, false)
|
||||||
|
Truth.assertThat(someoneDeniedYourKnock).isEqualTo("$otherName rejected your request to join")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Membership change - others`() {
|
||||||
|
val otherChanges = arrayOf(MembershipChange.NONE, MembershipChange.ERROR, MembershipChange.NOT_IMPLEMENTED)
|
||||||
|
|
||||||
|
val results = otherChanges.map { change ->
|
||||||
|
val content = RoomMembershipContent(A_USER_ID, change)
|
||||||
|
val event = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content)
|
||||||
|
val result = formatter.processMessageItem(event, false)
|
||||||
|
change to result
|
||||||
|
}
|
||||||
|
val expected = otherChanges.map { it to null }
|
||||||
|
Truth.assertThat(results).isEqualTo(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Room State
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Room state change - avatar`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val changedContent = StateContent("", OtherState.RoomAvatar("new_avatar"))
|
||||||
|
val removedContent = StateContent("", OtherState.RoomAvatar(null))
|
||||||
|
|
||||||
|
val youChangedRoomAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
|
||||||
|
val youChangedRoomAvatar = formatter.processMessageItem(youChangedRoomAvatarEvent, false)
|
||||||
|
Truth.assertThat(youChangedRoomAvatar).isEqualTo("You changed the room avatar")
|
||||||
|
|
||||||
|
val someoneChangedRoomAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent)
|
||||||
|
val someoneChangedRoomAvatar = formatter.processMessageItem(someoneChangedRoomAvatarEvent, false)
|
||||||
|
Truth.assertThat(someoneChangedRoomAvatar).isEqualTo("$otherName changed the room avatar")
|
||||||
|
|
||||||
|
val youRemovedRoomAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent)
|
||||||
|
val youRemovedRoomAvatar = formatter.processMessageItem(youRemovedRoomAvatarEvent, false)
|
||||||
|
Truth.assertThat(youRemovedRoomAvatar).isEqualTo("You removed the room avatar")
|
||||||
|
|
||||||
|
val someoneRemovedRoomAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent)
|
||||||
|
val someoneRemovedRoomAvatar = formatter.processMessageItem(someoneRemovedRoomAvatarEvent, false)
|
||||||
|
Truth.assertThat(someoneRemovedRoomAvatar).isEqualTo("$otherName removed the room avatar")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Room state change - create`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val content = StateContent("", OtherState.RoomCreate)
|
||||||
|
|
||||||
|
val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content)
|
||||||
|
val youCreatedRoom = formatter.processMessageItem(youCreatedRoomMessage, false)
|
||||||
|
Truth.assertThat(youCreatedRoom).isEqualTo("You created the room")
|
||||||
|
|
||||||
|
val someoneCreatedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = content)
|
||||||
|
val someoneCreatedRoom = formatter.processMessageItem(someoneCreatedRoomEvent, false)
|
||||||
|
Truth.assertThat(someoneCreatedRoom).isEqualTo("$otherName created the room")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Room state change - encryption`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val content = StateContent("", OtherState.RoomEncryption)
|
||||||
|
|
||||||
|
val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content)
|
||||||
|
val youCreatedRoom = formatter.processMessageItem(youCreatedRoomMessage, false)
|
||||||
|
Truth.assertThat(youCreatedRoom).isEqualTo("Encryption enabled")
|
||||||
|
|
||||||
|
val someoneCreatedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = content)
|
||||||
|
val someoneCreatedRoom = formatter.processMessageItem(someoneCreatedRoomEvent, false)
|
||||||
|
Truth.assertThat(someoneCreatedRoom).isEqualTo("Encryption enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Room state change - room name`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val newName = "New name"
|
||||||
|
val changedContent = StateContent("", OtherState.RoomName(newName))
|
||||||
|
val removedContent = StateContent("", OtherState.RoomName(null))
|
||||||
|
|
||||||
|
val youChangedRoomNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
|
||||||
|
val youChangedRoomName = formatter.processMessageItem(youChangedRoomNameEvent, false)
|
||||||
|
Truth.assertThat(youChangedRoomName).isEqualTo("You changed the room name to: $newName")
|
||||||
|
|
||||||
|
val someoneChangedRoomNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent)
|
||||||
|
val someoneChangedRoomName = formatter.processMessageItem(someoneChangedRoomNameEvent, false)
|
||||||
|
Truth.assertThat(someoneChangedRoomName).isEqualTo("$otherName changed the room name to: $newName")
|
||||||
|
|
||||||
|
val youRemovedRoomNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent)
|
||||||
|
val youRemovedRoomName = formatter.processMessageItem(youRemovedRoomNameEvent, false)
|
||||||
|
Truth.assertThat(youRemovedRoomName).isEqualTo("You removed the room name")
|
||||||
|
|
||||||
|
val someoneRemovedRoomNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent)
|
||||||
|
val someoneRemovedRoomName = formatter.processMessageItem(someoneRemovedRoomNameEvent, false)
|
||||||
|
Truth.assertThat(someoneRemovedRoomName).isEqualTo("$otherName removed the room name")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Room state change - third party invite`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val inviteeName = "Alice"
|
||||||
|
val changedContent = StateContent("", OtherState.RoomThirdPartyInvite(inviteeName))
|
||||||
|
val removedContent = StateContent("", OtherState.RoomThirdPartyInvite(null))
|
||||||
|
|
||||||
|
val youInvitedSomeoneEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
|
||||||
|
val youInvitedSomeone = formatter.processMessageItem(youInvitedSomeoneEvent, false)
|
||||||
|
Truth.assertThat(youInvitedSomeone).isEqualTo("You sent an invitation to $inviteeName to join the room")
|
||||||
|
|
||||||
|
val someoneInvitedSomeoneEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent)
|
||||||
|
val someoneInvitedSomeone = formatter.processMessageItem(someoneInvitedSomeoneEvent, false)
|
||||||
|
Truth.assertThat(someoneInvitedSomeone).isEqualTo("$otherName sent an invitation to $inviteeName to join the room")
|
||||||
|
|
||||||
|
val youInvitedNoOneEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent)
|
||||||
|
val youInvitedNoOne = formatter.processMessageItem(youInvitedNoOneEvent, false)
|
||||||
|
Truth.assertThat(youInvitedNoOne).isNull()
|
||||||
|
|
||||||
|
val someoneInvitedNoOneEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent)
|
||||||
|
val someoneInvitedNoOne = formatter.processMessageItem(someoneInvitedNoOneEvent, false)
|
||||||
|
Truth.assertThat(someoneInvitedNoOne).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Room state change - room topic`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val roomTopic = "New topic"
|
||||||
|
val changedContent = StateContent("", OtherState.RoomTopic(roomTopic))
|
||||||
|
val removedContent = StateContent("", OtherState.RoomTopic(null))
|
||||||
|
|
||||||
|
val youChangedRoomTopicEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
|
||||||
|
val youChangedRoomTopic = formatter.processMessageItem(youChangedRoomTopicEvent, false)
|
||||||
|
Truth.assertThat(youChangedRoomTopic).isEqualTo("You changed the topic to: $roomTopic")
|
||||||
|
|
||||||
|
val someoneChangedRoomTopicEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent)
|
||||||
|
val someoneChangedRoomTopic = formatter.processMessageItem(someoneChangedRoomTopicEvent, false)
|
||||||
|
Truth.assertThat(someoneChangedRoomTopic).isEqualTo("$otherName changed the topic to: $roomTopic")
|
||||||
|
|
||||||
|
val youRemovedRoomTopicEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent)
|
||||||
|
val youRemovedRoomTopic = formatter.processMessageItem(youRemovedRoomTopicEvent, false)
|
||||||
|
Truth.assertThat(youRemovedRoomTopic).isEqualTo("You removed the room topic")
|
||||||
|
|
||||||
|
val someoneRemovedRoomTopicEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent)
|
||||||
|
val someoneRemovedRoomTopic = formatter.processMessageItem(someoneRemovedRoomTopicEvent, false)
|
||||||
|
Truth.assertThat(someoneRemovedRoomTopic).isEqualTo("$otherName removed the room topic")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Room state change - others must return null`() {
|
||||||
|
val otherStates = arrayOf(
|
||||||
|
OtherState.PolicyRuleRoom, OtherState.PolicyRuleServer, OtherState.PolicyRuleUser, OtherState.RoomAliases, OtherState.RoomCanonicalAlias,
|
||||||
|
OtherState.RoomGuestAccess, OtherState.RoomHistoryVisibility, OtherState.RoomJoinRules, OtherState.RoomPinnedEvents, OtherState.RoomPowerLevels,
|
||||||
|
OtherState.RoomServerAcl, OtherState.RoomTombstone, OtherState.SpaceChild, OtherState.SpaceParent, OtherState.Custom("custom_event_type")
|
||||||
|
)
|
||||||
|
|
||||||
|
val results = otherStates.map { state ->
|
||||||
|
val content = StateContent("", state)
|
||||||
|
val event = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content)
|
||||||
|
val result = formatter.processMessageItem(event, false)
|
||||||
|
state to result
|
||||||
|
}
|
||||||
|
val expected = otherStates.map { it to null }
|
||||||
|
Truth.assertThat(results).isEqualTo(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Profile change
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Profile change - avatar`() {
|
||||||
|
val otherName = "Someone"
|
||||||
|
val changedContent = aProfileChangeMessageContent(avatarUrl = "new_avatar_url", prevAvatarUrl = "old_avatar_url")
|
||||||
|
val setContent = aProfileChangeMessageContent(avatarUrl = "new_avatar_url", prevAvatarUrl = null)
|
||||||
|
val removedContent = aProfileChangeMessageContent(avatarUrl = null, prevAvatarUrl = "old_avatar_url")
|
||||||
|
val invalidContent = aProfileChangeMessageContent(avatarUrl = null, prevAvatarUrl = null)
|
||||||
|
val sameContent = aProfileChangeMessageContent(avatarUrl = "same_avatar_url", prevAvatarUrl = "same_avatar_url")
|
||||||
|
|
||||||
|
val youChangedAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
|
||||||
|
val youChangedAvatar = formatter.processMessageItem(youChangedAvatarEvent, false)
|
||||||
|
Truth.assertThat(youChangedAvatar).isEqualTo("You changed your avatar")
|
||||||
|
|
||||||
|
val someoneChangeAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent)
|
||||||
|
val someoneChangeAvatar = formatter.processMessageItem(someoneChangeAvatarEvent, false)
|
||||||
|
Truth.assertThat(someoneChangeAvatar).isEqualTo("$otherName changed their avatar")
|
||||||
|
|
||||||
|
val youSetAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = setContent)
|
||||||
|
val youSetAvatar = formatter.processMessageItem(youSetAvatarEvent, false)
|
||||||
|
Truth.assertThat(youSetAvatar).isEqualTo("You changed your avatar")
|
||||||
|
|
||||||
|
val someoneSetAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = setContent)
|
||||||
|
val someoneSetAvatar = formatter.processMessageItem(someoneSetAvatarEvent, false)
|
||||||
|
Truth.assertThat(someoneSetAvatar).isEqualTo("$otherName changed their avatar")
|
||||||
|
|
||||||
|
val youRemovedAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent)
|
||||||
|
val youRemovedAvatar = formatter.processMessageItem(youRemovedAvatarEvent, false)
|
||||||
|
Truth.assertThat(youRemovedAvatar).isEqualTo("You changed your avatar")
|
||||||
|
|
||||||
|
val someoneRemovedAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent)
|
||||||
|
val someoneRemovedAvatar = formatter.processMessageItem(someoneRemovedAvatarEvent, false)
|
||||||
|
Truth.assertThat(someoneRemovedAvatar).isEqualTo("$otherName changed their avatar")
|
||||||
|
|
||||||
|
val unchangedEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = sameContent)
|
||||||
|
val unchangedResult = formatter.processMessageItem(unchangedEvent, false)
|
||||||
|
Truth.assertThat(unchangedResult).isNull()
|
||||||
|
|
||||||
|
val invalidEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = invalidContent)
|
||||||
|
val invalidResult = formatter.processMessageItem(invalidEvent, false)
|
||||||
|
Truth.assertThat(invalidResult).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Profile change - display name`() {
|
||||||
|
val newDisplayName = "New"
|
||||||
|
val oldDisplayName = "Old"
|
||||||
|
val otherName = "Someone"
|
||||||
|
val changedContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = oldDisplayName)
|
||||||
|
val setContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = null)
|
||||||
|
val removedContent = aProfileChangeMessageContent(displayName = null, prevDisplayName = oldDisplayName)
|
||||||
|
val sameContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = newDisplayName)
|
||||||
|
val invalidContent = aProfileChangeMessageContent(displayName = null, prevDisplayName = null)
|
||||||
|
|
||||||
|
val youChangedDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
|
||||||
|
val youChangedDisplayName = formatter.processMessageItem(youChangedDisplayNameEvent, false)
|
||||||
|
Truth.assertThat(youChangedDisplayName).isEqualTo("You changed your display name from $oldDisplayName to $newDisplayName")
|
||||||
|
|
||||||
|
val someoneChangedDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent)
|
||||||
|
val someoneChangedDisplayName = formatter.processMessageItem(someoneChangedDisplayNameEvent, false)
|
||||||
|
Truth.assertThat(someoneChangedDisplayName).isEqualTo("$otherName changed their display name from $oldDisplayName to $newDisplayName")
|
||||||
|
|
||||||
|
val youSetDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = setContent)
|
||||||
|
val youSetDisplayName = formatter.processMessageItem(youSetDisplayNameEvent, false)
|
||||||
|
Truth.assertThat(youSetDisplayName).isEqualTo("You set your display name to $newDisplayName")
|
||||||
|
|
||||||
|
val someoneSetDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = setContent)
|
||||||
|
val someoneSetDisplayName = formatter.processMessageItem(someoneSetDisplayNameEvent, false)
|
||||||
|
Truth.assertThat(someoneSetDisplayName).isEqualTo("$otherName set their display name to $newDisplayName")
|
||||||
|
|
||||||
|
val youRemovedDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent)
|
||||||
|
val youRemovedDisplayName = formatter.processMessageItem(youRemovedDisplayNameEvent, false)
|
||||||
|
Truth.assertThat(youRemovedDisplayName).isEqualTo("You removed your display name (it was $oldDisplayName)")
|
||||||
|
|
||||||
|
val someoneRemovedDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent)
|
||||||
|
val someoneRemovedDisplayName = formatter.processMessageItem(someoneRemovedDisplayNameEvent, false)
|
||||||
|
Truth.assertThat(someoneRemovedDisplayName).isEqualTo("$otherName removed their display name (it was $oldDisplayName)")
|
||||||
|
|
||||||
|
val unchangedEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = sameContent)
|
||||||
|
val unchangedResult = formatter.processMessageItem(unchangedEvent, false)
|
||||||
|
Truth.assertThat(unchangedResult).isNull()
|
||||||
|
|
||||||
|
val invalidEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = invalidContent)
|
||||||
|
val invalidResult = formatter.processMessageItem(invalidEvent, false)
|
||||||
|
Truth.assertThat(invalidResult).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = "en")
|
||||||
|
fun `Profile change - display name & avatar`() {
|
||||||
|
val newDisplayName = "New"
|
||||||
|
val oldDisplayName = "Old"
|
||||||
|
val changedContent = aProfileChangeMessageContent(
|
||||||
|
displayName = newDisplayName,
|
||||||
|
prevDisplayName = oldDisplayName,
|
||||||
|
avatarUrl = "new_avatar_url",
|
||||||
|
prevAvatarUrl = "old_avatar_url",
|
||||||
|
)
|
||||||
|
val invalidContent = aProfileChangeMessageContent(
|
||||||
|
displayName = null,
|
||||||
|
prevDisplayName = null,
|
||||||
|
avatarUrl = null,
|
||||||
|
prevAvatarUrl = null,
|
||||||
|
)
|
||||||
|
val sameContent = aProfileChangeMessageContent(
|
||||||
|
displayName = newDisplayName,
|
||||||
|
prevDisplayName = newDisplayName,
|
||||||
|
avatarUrl = "same_avatar_url",
|
||||||
|
prevAvatarUrl = "same_avatar_url",
|
||||||
|
)
|
||||||
|
|
||||||
|
val youChangedBothEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
|
||||||
|
val youChangedBoth = formatter.processMessageItem(youChangedBothEvent, false)
|
||||||
|
Truth.assertThat(youChangedBoth).isEqualTo("You changed your display name from $oldDisplayName to $newDisplayName\n(avatar was changed too)")
|
||||||
|
|
||||||
|
val invalidContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = invalidContent)
|
||||||
|
val invalidMessage = formatter.processMessageItem(invalidContentEvent, false)
|
||||||
|
Truth.assertThat(invalidMessage).isNull()
|
||||||
|
|
||||||
|
val sameContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = sameContent)
|
||||||
|
val sameMessage = formatter.processMessageItem(sameContentEvent, false)
|
||||||
|
Truth.assertThat(sameMessage).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
private fun createRoomEvent(sentByYou: Boolean, senderDisplayName: String?, content: EventContent): EventTimelineItem {
|
||||||
|
val sender = if (sentByYou) A_USER_ID else UserId("someone_else")
|
||||||
|
val profile = ProfileTimelineDetails.Ready(senderDisplayName, false, null)
|
||||||
|
return anEventTimelineItem(content = content, senderProfile = profile, sender = sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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 io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
|
|
||||||
|
class FakeRoomLastMessageFormatter : RoomLastMessageFormatter {
|
||||||
|
|
||||||
|
private var processMessageItemResult: CharSequence? = null
|
||||||
|
override fun processMessageItem(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? {
|
||||||
|
return processMessageItemResult
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenRoomSummaryResult(result: CharSequence?) {
|
||||||
|
processMessageItemResult = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,14 +21,13 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.libraries.dateformatter.api.LastMessageFormatter
|
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageFormatter
|
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
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_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
|
@ -48,6 +47,7 @@ class RoomListPresenterTests {
|
||||||
val presenter = RoomListPresenter(
|
val presenter = RoomListPresenter(
|
||||||
FakeMatrixClient(A_SESSION_ID),
|
FakeMatrixClient(A_SESSION_ID),
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -73,6 +73,7 @@ class RoomListPresenterTests {
|
||||||
userAvatarURLString = Result.failure(AN_EXCEPTION),
|
userAvatarURLString = Result.failure(AN_EXCEPTION),
|
||||||
),
|
),
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -92,6 +93,7 @@ class RoomListPresenterTests {
|
||||||
val presenter = RoomListPresenter(
|
val presenter = RoomListPresenter(
|
||||||
FakeMatrixClient(A_SESSION_ID),
|
FakeMatrixClient(A_SESSION_ID),
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -115,6 +117,7 @@ class RoomListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource
|
roomSummaryDataSource = roomSummaryDataSource
|
||||||
),
|
),
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -143,6 +146,7 @@ class RoomListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource
|
roomSummaryDataSource = roomSummaryDataSource
|
||||||
),
|
),
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -176,6 +180,7 @@ class RoomListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource
|
roomSummaryDataSource = roomSummaryDataSource
|
||||||
),
|
),
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -220,6 +225,7 @@ class RoomListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource
|
roomSummaryDataSource = roomSummaryDataSource
|
||||||
),
|
),
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService().apply {
|
FakeSessionVerificationService().apply {
|
||||||
givenIsReady(true)
|
givenIsReady(true)
|
||||||
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
|
|
@ -245,6 +251,7 @@ class RoomListPresenterTests {
|
||||||
roomSummaryDataSource = roomSummaryDataSource
|
roomSummaryDataSource = roomSummaryDataSource
|
||||||
),
|
),
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService().apply {
|
FakeSessionVerificationService().apply {
|
||||||
givenIsReady(true)
|
givenIsReady(true)
|
||||||
givenVerificationFlowState(VerificationFlowState.Finished)
|
givenVerificationFlowState(VerificationFlowState.Finished)
|
||||||
|
|
@ -261,8 +268,8 @@ class RoomListPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDateFormatter(): LastMessageFormatter {
|
private fun createDateFormatter(): LastMessageTimestampFormatter {
|
||||||
return FakeLastMessageFormatter().apply {
|
return FakeLastMessageTimestampFormatter().apply {
|
||||||
givenFormat(A_FORMATTED_DATE)
|
givenFormat(A_FORMATTED_DATE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +283,7 @@ private val aRoomListRoomSummary = RoomListRoomSummary(
|
||||||
name = A_ROOM_NAME,
|
name = A_ROOM_NAME,
|
||||||
hasUnread = true,
|
hasUnread = true,
|
||||||
timestamp = A_FORMATTED_DATE,
|
timestamp = A_FORMATTED_DATE,
|
||||||
lastMessage = A_MESSAGE,
|
lastMessage = "",
|
||||||
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME),
|
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME),
|
||||||
isPlaceholder = false,
|
isPlaceholder = false,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ test_orchestrator = "androidx.test:orchestrator:1.4.2"
|
||||||
test_turbine = "app.cash.turbine:turbine:0.12.1"
|
test_turbine = "app.cash.turbine:turbine:0.12.1"
|
||||||
test_truth = "com.google.truth:truth:1.1.3"
|
test_truth = "com.google.truth:truth:1.1.3"
|
||||||
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.10"
|
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.10"
|
||||||
|
test_robolectric = "org.robolectric:robolectric:4.9"
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.dateformatter.api
|
package io.element.android.libraries.dateformatter.api
|
||||||
|
|
||||||
interface LastMessageFormatter {
|
interface LastMessageTimestampFormatter {
|
||||||
fun format(timestamp: Long?): String
|
fun format(timestamp: Long?): String
|
||||||
}
|
}
|
||||||
|
|
@ -17,15 +17,15 @@
|
||||||
package io.element.android.libraries.dateformatter.impl
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
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 io.element.android.libraries.di.AppScope
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultLastMessageFormatter @Inject constructor(
|
class DefaultLastMessageTimestampFormatter @Inject constructor(
|
||||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||||
private val dateFormatters: DateFormatters,
|
private val dateFormatters: DateFormatters,
|
||||||
) : LastMessageFormatter {
|
) : LastMessageTimestampFormatter {
|
||||||
|
|
||||||
override fun format(timestamp: Long?): String {
|
override fun format(timestamp: Long?): String {
|
||||||
if (timestamp == null) return ""
|
if (timestamp == null) return ""
|
||||||
|
|
@ -17,14 +17,14 @@
|
||||||
package io.element.android.libraries.dateformatter.impl
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
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 io.element.android.libraries.dateformatter.test.FakeClock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class DefaultLastMessageFormatterTest {
|
class DefaultLastMessageTimestampFormatterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test null`() {
|
fun `test null`() {
|
||||||
|
|
@ -100,10 +100,10 @@ class DefaultLastMessageFormatterTest {
|
||||||
/**
|
/**
|
||||||
* Create DefaultLastMessageFormatter and set current time to the provided date.
|
* 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 clock = FakeClock().also { it.givenInstant(Instant.parse(currentDate)) }
|
||||||
val localDateTimeProvider = LocalDateTimeProvider(clock, TimeZone.UTC)
|
val localDateTimeProvider = LocalDateTimeProvider(clock, TimeZone.UTC)
|
||||||
val dateFormatters = DateFormatters(Locale.US, 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
|
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 = ""
|
private var format = ""
|
||||||
fun givenFormat(format: String) {
|
fun givenFormat(format: String) {
|
||||||
this.format = format
|
this.format = format
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.libraries.matrix.api.room
|
package io.element.android.libraries.matrix.api.room
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||||
|
|
||||||
sealed interface RoomSummary {
|
sealed interface RoomSummary {
|
||||||
data class Empty(val identifier: String) : RoomSummary
|
data class Empty(val identifier: String) : RoomSummary
|
||||||
|
|
@ -35,7 +36,7 @@ data class RoomSummaryDetails(
|
||||||
val name: String,
|
val name: String,
|
||||||
val isDirect: Boolean,
|
val isDirect: Boolean,
|
||||||
val avatarURLString: String?,
|
val avatarURLString: String?,
|
||||||
val lastMessage: CharSequence?,
|
val lastMessage: RoomMessage?,
|
||||||
val lastMessageTimestamp: Long?,
|
val lastMessageTimestamp: Long?,
|
||||||
val unreadNotificationCount: Int,
|
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.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
|
|
||||||
data class RoomMessage(
|
data class RoomMessage(
|
||||||
val eventId: EventId,
|
val eventId: EventId,
|
||||||
val body: String,
|
val event: EventTimelineItem,
|
||||||
val sender: UserId,
|
val sender: UserId,
|
||||||
val originServerTs: Long,
|
val originServerTs: Long,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.room
|
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.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
|
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
|
||||||
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
|
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 {
|
val latestRoomMessage = slidingSyncRoom.latestRoomMessage()?.use {
|
||||||
roomMessageFactory.create(it)
|
roomMessageFactory.create(it)
|
||||||
}
|
}
|
||||||
val computedLastMessage = when {
|
|
||||||
latestRoomMessage == null -> null
|
|
||||||
slidingSyncRoom.isDm() == true -> latestRoomMessage.body
|
|
||||||
else -> "${latestRoomMessage.sender.value}: ${latestRoomMessage.body}"
|
|
||||||
}
|
|
||||||
return RoomSummaryDetails(
|
return RoomSummaryDetails(
|
||||||
roomId = RoomId(slidingSyncRoom.roomId()),
|
roomId = RoomId(slidingSyncRoom.roomId()),
|
||||||
name = slidingSyncRoom.name() ?: slidingSyncRoom.roomId(),
|
name = slidingSyncRoom.name() ?: slidingSyncRoom.roomId(),
|
||||||
isDirect = slidingSyncRoom.isDm() ?: false,
|
isDirect = slidingSyncRoom.isDm() ?: false,
|
||||||
avatarURLString = room?.avatarUrl(),
|
avatarURLString = room?.avatarUrl(),
|
||||||
unreadNotificationCount = slidingSyncRoom.unreadNotifications().use { it.notificationCount().toInt() },
|
unreadNotificationCount = slidingSyncRoom.unreadNotifications().use { it.notificationCount().toInt() },
|
||||||
lastMessage = computedLastMessage,
|
lastMessage = latestRoomMessage,
|
||||||
lastMessageTimestamp = latestRoomMessage?.originServerTs
|
lastMessageTimestamp = latestRoomMessage?.originServerTs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,19 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.room.message
|
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 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 {
|
class RoomMessageFactory {
|
||||||
fun create(eventTimelineItem: EventTimelineItem?): RoomMessage? {
|
fun create(eventTimelineItem: RustEventTimelineItem?): RoomMessage? {
|
||||||
eventTimelineItem ?: return null
|
eventTimelineItem ?: return null
|
||||||
|
val mappedTimelineItem = EventTimelineItemMapper().map(eventTimelineItem)
|
||||||
return RoomMessage(
|
return RoomMessage(
|
||||||
eventId = EventId(eventTimelineItem.eventId() ?: ""),
|
eventId = mappedTimelineItem.eventId!!,
|
||||||
body = eventTimelineItem.content().asMessage()?.body() ?: "",
|
event = mappedTimelineItem,
|
||||||
sender = UserId(eventTimelineItem.sender()),
|
sender = mappedTimelineItem.sender,
|
||||||
originServerTs = eventTimelineItem.timestamp().toLong()
|
originServerTs = mappedTimelineItem.timestamp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ val A_SPACE_ID = SpaceId("!aSpaceId")
|
||||||
val A_ROOM_ID = RoomId("!aRoomId")
|
val A_ROOM_ID = RoomId("!aRoomId")
|
||||||
val A_THREAD_ID = ThreadId("\$aThreadId")
|
val A_THREAD_ID = ThreadId("\$aThreadId")
|
||||||
val AN_EVENT_ID = EventId("\$anEventId")
|
val AN_EVENT_ID = EventId("\$anEventId")
|
||||||
|
const val A_UNIQUE_ID = "aUniqueId"
|
||||||
|
|
||||||
const val A_ROOM_NAME = "A room name"
|
const val A_ROOM_NAME = "A room name"
|
||||||
const val A_MESSAGE = "Hello world!"
|
const val A_MESSAGE = "Hello world!"
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,33 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.test.room
|
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.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.RoomSummary
|
||||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
|
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_MESSAGE
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
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_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(
|
fun aRoomSummaryFilled(
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
name: String = A_ROOM_NAME,
|
name: String = A_ROOM_NAME,
|
||||||
isDirect: Boolean = false,
|
isDirect: Boolean = false,
|
||||||
avatarURLString: String? = null,
|
avatarURLString: String? = null,
|
||||||
lastMessage: CharSequence? = A_MESSAGE,
|
lastMessage: RoomMessage? = aRoomMessage(),
|
||||||
lastMessageTimestamp: Long? = null,
|
lastMessageTimestamp: Long? = null,
|
||||||
unreadNotificationCount: Int = 2,
|
unreadNotificationCount: Int = 2,
|
||||||
) = RoomSummary.Filled(
|
) = RoomSummary.Filled(
|
||||||
|
|
@ -48,7 +62,7 @@ fun aRoomSummaryDetail(
|
||||||
name: String = A_ROOM_NAME,
|
name: String = A_ROOM_NAME,
|
||||||
isDirect: Boolean = false,
|
isDirect: Boolean = false,
|
||||||
avatarURLString: String? = null,
|
avatarURLString: String? = null,
|
||||||
lastMessage: CharSequence? = A_MESSAGE,
|
lastMessage: RoomMessage? = aRoomMessage(),
|
||||||
lastMessageTimestamp: Long? = null,
|
lastMessageTimestamp: Long? = null,
|
||||||
unreadNotificationCount: Int = 2,
|
unreadNotificationCount: Int = 2,
|
||||||
) = RoomSummaryDetails(
|
) = RoomSummaryDetails(
|
||||||
|
|
@ -60,3 +74,65 @@ fun aRoomSummaryDetail(
|
||||||
lastMessageTimestamp = lastMessageTimestamp,
|
lastMessageTimestamp = lastMessageTimestamp,
|
||||||
unreadNotificationCount = unreadNotificationCount,
|
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 -->
|
<!-- Create room -->
|
||||||
<string name="search_for_someone">Search for someone</string>
|
<string name="search_for_someone">Search for someone</string>
|
||||||
<string name="new_room">New room</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_initial">Open an existing session</string>
|
||||||
<string name="verification_title_waiting">Waiting to accept request</string>
|
<string name="verification_title_waiting">Waiting to accept request</string>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
import io.element.android.libraries.designsystem.theme.ElementTheme
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
|
@ -72,7 +73,7 @@ class MainActivity : ComponentActivity() {
|
||||||
val sessionId = matrixAuthenticationService.getLatestSessionId()!!
|
val sessionId = matrixAuthenticationService.getLatestSessionId()!!
|
||||||
matrixAuthenticationService.restoreSession(sessionId).getOrNull()
|
matrixAuthenticationService.restoreSession(sessionId).getOrNull()
|
||||||
}
|
}
|
||||||
RoomListScreen(matrixClient = matrixClient!!).Content(modifier)
|
RoomListScreen(LocalContext.current, matrixClient!!).Content(modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,15 @@
|
||||||
|
|
||||||
package io.element.android.samples.minimal
|
package io.element.android.samples.minimal
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.features.roomlist.impl.DefaultRoomLastMessageFormatter
|
||||||
import io.element.android.features.roomlist.impl.RoomListPresenter
|
import io.element.android.features.roomlist.impl.RoomListPresenter
|
||||||
import io.element.android.features.roomlist.impl.RoomListView
|
import io.element.android.features.roomlist.impl.RoomListView
|
||||||
import io.element.android.libraries.dateformatter.impl.DateFormatters
|
import io.element.android.libraries.dateformatter.impl.DateFormatters
|
||||||
import io.element.android.libraries.dateformatter.impl.DefaultLastMessageFormatter
|
import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter
|
||||||
import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
|
import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -32,16 +34,21 @@ import kotlinx.datetime.TimeZone
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class RoomListScreen(
|
class RoomListScreen(
|
||||||
private val matrixClient: MatrixClient
|
context: Context,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val clock = Clock.System
|
private val clock = Clock.System
|
||||||
private val locale = Locale.getDefault()
|
private val locale = Locale.getDefault()
|
||||||
private val timeZone = TimeZone.currentSystemDefault()
|
private val timeZone = TimeZone.currentSystemDefault()
|
||||||
private val dateTimeProvider = LocalDateTimeProvider(clock, timeZone)
|
private val dateTimeProvider = LocalDateTimeProvider(clock, timeZone)
|
||||||
private val dateFormatters = DateFormatters(locale, clock, timeZone)
|
private val dateFormatters = DateFormatters(locale, clock, timeZone)
|
||||||
private val sessionVerificationService = matrixClient.sessionVerificationService()
|
private val sessionVerificationService = matrixClient.sessionVerificationService()
|
||||||
private val presenter = RoomListPresenter(matrixClient, DefaultLastMessageFormatter(dateTimeProvider, dateFormatters), sessionVerificationService)
|
private val presenter = RoomListPresenter(
|
||||||
|
matrixClient,
|
||||||
|
DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters),
|
||||||
|
DefaultRoomLastMessageFormatter(context, matrixClient),
|
||||||
|
sessionVerificationService
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Content(modifier: Modifier = Modifier) {
|
fun Content(modifier: Modifier = Modifier) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue