Skip to content
Snippets Groups Projects
Commit 5e8f13d9 authored by Benedict's avatar Benedict
Browse files

Merge branch '266-deletion-of-messages-is-documented-with-wrong-user-name' into 'main'

Fixed redacted naming.

See merge request connect2x/trixnity-messenger/trixnity-messenger!97
parents 5ee298ec 826364a5
No related branches found
No related tags found
No related merge requests found
...@@ -152,6 +152,16 @@ abstract class I18n(languages: Languages, settings: MatrixMessengerSettingsHolde ...@@ -152,6 +152,16 @@ abstract class I18n(languages: Languages, settings: MatrixMessengerSettingsHolde
DE - "Nachricht wurde von $username gelöscht" DE - "Nachricht wurde von $username gelöscht"
} }
fun eventMessageRedactedByMe() = translate {
EN - "You deleted this message"
DE - "Sie haben diese Nachricht gelöscht"
}
fun eventMessageRedactedByUnknown() = translate {
EN - "This message has been deleted"
DE - "Diese Nachricht ist gelöscht worden"
}
fun eventRoomCreated(username: String, groupOrChat: String) = translate { fun eventRoomCreated(username: String, groupOrChat: String) = translate {
EN - "$username has created $groupOrChat" EN - "$username has created $groupOrChat"
DE - "$username hat $groupOrChat erstellt" DE - "$username hat $groupOrChat erstellt"
......
...@@ -6,10 +6,13 @@ import de.connect2x.trixnity.messenger.viewmodel.i18n ...@@ -6,10 +6,13 @@ import de.connect2x.trixnity.messenger.viewmodel.i18n
import de.connect2x.trixnity.messenger.viewmodel.util.formatDate import de.connect2x.trixnity.messenger.viewmodel.util.formatDate
import de.connect2x.trixnity.messenger.viewmodel.util.formatTime import de.connect2x.trixnity.messenger.viewmodel.util.formatTime
import de.connect2x.trixnity.messenger.viewmodel.util.timezone import de.connect2x.trixnity.messenger.viewmodel.util.timezone
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
...@@ -17,9 +20,13 @@ import kotlinx.datetime.TimeZone ...@@ -17,9 +20,13 @@ import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import net.folivo.trixnity.client.store.TimelineEvent import net.folivo.trixnity.client.store.TimelineEvent
import net.folivo.trixnity.client.store.unsigned import net.folivo.trixnity.client.store.unsigned
import net.folivo.trixnity.client.user
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.RedactedEventContent import net.folivo.trixnity.core.model.events.RedactedEventContent
import net.folivo.trixnity.core.model.events.originTimestampOrNull import net.folivo.trixnity.core.model.events.originTimestampOrNull
private val log = KotlinLogging.logger { }
interface RedactedMessageViewModelFactory { interface RedactedMessageViewModelFactory {
fun create( fun create(
...@@ -35,6 +42,8 @@ interface RedactedMessageViewModelFactory { ...@@ -35,6 +42,8 @@ interface RedactedMessageViewModelFactory {
showSender: Flow<Boolean>, showSender: Flow<Boolean>,
sender: Flow<UserInfoElement>, sender: Flow<UserInfoElement>,
invitation: Flow<String?>, invitation: Flow<String?>,
selectedRoomId: RoomId,
redactedBy: UserId?
): RedactedMessageViewModel { ): RedactedMessageViewModel {
return RedactedMessageViewModelImpl( return RedactedMessageViewModelImpl(
viewModelContext, viewModelContext,
...@@ -46,9 +55,11 @@ interface RedactedMessageViewModelFactory { ...@@ -46,9 +55,11 @@ interface RedactedMessageViewModelFactory {
isByMe, isByMe,
showChatBubbleEdge, showChatBubbleEdge,
showBigGap, showBigGap,
selectedRoomId,
showSender, showSender,
sender, sender,
invitation, invitation,
redactedBy
) )
} }
...@@ -70,9 +81,11 @@ open class RedactedMessageViewModelImpl( ...@@ -70,9 +81,11 @@ open class RedactedMessageViewModelImpl(
override val isByMe: Boolean, override val isByMe: Boolean,
override val showChatBubbleEdge: Boolean, override val showChatBubbleEdge: Boolean,
override val showBigGap: Boolean, override val showBigGap: Boolean,
selectedRoomId: RoomId,
showSender: Flow<Boolean>, showSender: Flow<Boolean>,
sender: Flow<UserInfoElement>, sender: Flow<UserInfoElement>,
invitation: Flow<String?>, invitation: Flow<String?>,
redactedBy: UserId?,
) : RedactedMessageViewModel, MatrixClientViewModelContext by viewModelContext { ) : RedactedMessageViewModel, MatrixClientViewModelContext by viewModelContext {
override val invitation: StateFlow<String?> = override val invitation: StateFlow<String?> =
invitation.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) invitation.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
...@@ -81,15 +94,21 @@ open class RedactedMessageViewModelImpl( ...@@ -81,15 +94,21 @@ open class RedactedMessageViewModelImpl(
override val showSender: StateFlow<Boolean> = override val showSender: StateFlow<Boolean> =
showSender.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), true) showSender.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), true)
override val formattedMessage = sender.map { userInfo -> override val formattedMessage = when (redactedBy) {
i18n.eventMessageRedacted(userInfo.name) null -> MutableStateFlow(i18n.eventMessageRedactedByUnknown())
matrixClient.userId -> MutableStateFlow(i18n.eventMessageRedactedByMe())
else -> matrixClient.user.getById(selectedRoomId, redactedBy).map {
i18n.eventMessageRedacted(it?.name ?: redactedBy.full)
}.stateIn( }.stateIn(
coroutineScope, coroutineScope,
SharingStarted.WhileSubscribed(), SharingStarted.WhileSubscribed(),
i18n.eventMessageRedacted(i18n.commonUnknown()) i18n.eventMessageRedacted(i18n.commonUnknown())
) )
}
override val redactedAtDateTime: String? = timelineEvent?.unsigned?.redactedBecause?.originTimestampOrNull?.let { override val redactedAtDateTime: String? =
timelineEvent?.unsigned?.redactedBecause?.originTimestampOrNull?.let {
val localDateTime = Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.of(timezone())) val localDateTime = Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.of(timezone()))
"${formatDate(localDateTime)}, ${formatTime(localDateTime)}" "${formatDate(localDateTime)}, ${formatTime(localDateTime)}"
} }
......
...@@ -47,6 +47,7 @@ import net.folivo.trixnity.client.store.isReplaced ...@@ -47,6 +47,7 @@ import net.folivo.trixnity.client.store.isReplaced
import net.folivo.trixnity.client.store.isReplacing import net.folivo.trixnity.client.store.isReplacing
import net.folivo.trixnity.client.store.membership import net.folivo.trixnity.client.store.membership
import net.folivo.trixnity.client.store.roomId import net.folivo.trixnity.client.store.roomId
import net.folivo.trixnity.client.store.unsigned
import net.folivo.trixnity.client.user import net.folivo.trixnity.client.user
import net.folivo.trixnity.client.user.canSendEvent import net.folivo.trixnity.client.user.canSendEvent
import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.EventId
...@@ -503,6 +504,8 @@ open class TimelineElementHolderViewModelImpl( ...@@ -503,6 +504,8 @@ open class TimelineElementHolderViewModelImpl(
is RedactedEventContent -> { is RedactedEventContent -> {
log.trace { "Create redacted text message view model: ${event.id}" } log.trace { "Create redacted text message view model: ${event.id}" }
val redactedBy = timelineEvent.unsigned?.redactedBecause?.sender
get<RedactedMessageViewModelFactory>().create( get<RedactedMessageViewModelFactory>().create(
viewModelContext = this, viewModelContext = this,
timelineEvent = timelineEvent, timelineEvent = timelineEvent,
...@@ -513,9 +516,11 @@ open class TimelineElementHolderViewModelImpl( ...@@ -513,9 +516,11 @@ open class TimelineElementHolderViewModelImpl(
formattedDate = formatDate(receivedDateTime), formattedDate = formatDate(receivedDateTime),
showDateAbove = showDateAbove, showDateAbove = showDateAbove,
isByMe = isByMe, isByMe = isByMe,
selectedRoomId = selectedRoomId,
showChatBubbleEdge = showChatBubbleEdge, showChatBubbleEdge = showChatBubbleEdge,
showBigGap = showChatBubbleEdge, showBigGap = showChatBubbleEdge,
invitation = invitation, invitation = invitation,
redactedBy = redactedBy
) )
} }
......
package de.connect2x.trixnity.messenger.viewmodel.room.timeline.elements
import com.arkivanov.decompose.DefaultComponentContext
import com.arkivanov.essenty.lifecycle.LifecycleRegistry
import de.connect2x.trixnity.messenger.viewmodel.MatrixClientViewModelContextImpl
import de.connect2x.trixnity.messenger.viewmodel.UserInfoElement
import de.connect2x.trixnity.messenger.viewmodel.util.cancelNeverEndingCoroutines
import de.connect2x.trixnity.messenger.viewmodel.util.createTestDefaultTrixnityMessengerModules
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.core.test.testCoroutineScheduler
import io.kotest.matchers.shouldBe
import isTimelineEvent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.setMain
import net.folivo.trixnity.client.MatrixClient
import net.folivo.trixnity.client.room.RoomService
import net.folivo.trixnity.client.store.RoomUser
import net.folivo.trixnity.client.store.TimelineEvent
import net.folivo.trixnity.client.store.eventId
import net.folivo.trixnity.client.user.UserService
import net.folivo.trixnity.core.model.EventId
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.ClientEvent
import net.folivo.trixnity.core.model.events.MessageEventContent
import net.folivo.trixnity.core.model.events.RedactedEventContent
import net.folivo.trixnity.core.model.events.RoomEventContent
import net.folivo.trixnity.core.model.events.m.room.MemberEventContent
import net.folivo.trixnity.core.model.events.m.room.Membership
import org.kodein.mock.Mock
import org.kodein.mock.Mocker
import org.koin.dsl.koinApplication
import org.koin.dsl.module
import kotlin.coroutines.CoroutineContext
@OptIn(ExperimentalStdlibApi::class)
class RedactedMessageViewModelTest : ShouldSpec() {
private val roomId = RoomId("room1", "localhost")
private val ourUserId = UserId("bob", "localhost")
private val me = UserId("jonas", "localhost")
val eventId = EventId("0")
val mocker = Mocker()
@Mock
lateinit var matrixClientMock: MatrixClient
@Mock
lateinit var roomServiceMock: RoomService
@Mock
lateinit var userServiceMock: UserService
init {
coroutineTestScope = true
beforeTest {
mocker.reset()
injectMocks(mocker)
with(mocker) {
every { matrixClientMock.di } returns koinApplication {
modules(
module {
single { roomServiceMock }
single { userServiceMock }
}
)
}.koin
every { matrixClientMock.userId } returns me
mocker.every { userServiceMock.getById(roomId, ourUserId) } returns MutableStateFlow(
roomUser(me, "TestUser")
)
}
}
should("format generic message when redactedBy is null") {
val timelineEventFlow = timelineEvent(messageEvent(RedactedEventContent("somethig"), sender = ourUserId))
val cut = redactedMessageViewModel(
timelineEvent = timelineEventFlow,
coroutineContext = coroutineContext,
)
val subscriberJob = launch { cut.formattedMessage.collect {} }
testCoroutineScheduler.advanceUntilIdle()
cut.formattedMessage.value shouldBe "This message has been deleted"
subscriberJob.cancel()
cancelNeverEndingCoroutines()
}
should("append 'message deleted by me' when redactedBy ID matches current userId") {
val timelineEventFlow = timelineEvent(messageEvent(RedactedEventContent("somethig"), sender = me))
val cut = redactedMessageViewModel(
timelineEvent = timelineEventFlow,
coroutineContext = coroutineContext,
redactedBy = me
)
val subscriberJob = launch { cut.formattedMessage.collect {} }
testCoroutineScheduler.advanceUntilIdle()
cut.formattedMessage.value shouldBe "You deleted this message"
subscriberJob.cancel()
cancelNeverEndingCoroutines()
}
should("append 'redacted by other user' when redactedBy does not match current userId") {
val timelineEventFlow = timelineEvent(messageEvent(RedactedEventContent("somethig"), sender = ourUserId))
val cut = redactedMessageViewModel(
timelineEvent = timelineEventFlow,
coroutineContext = coroutineContext,
redactedBy = ourUserId
)
val subscriberJob = launch { cut.formattedMessage.collect {} }
testCoroutineScheduler.advanceUntilIdle()
cut.formattedMessage.value shouldBe "message has been deleted by TestUser"
subscriberJob.cancel()
cancelNeverEndingCoroutines()
}
}
@OptIn(ExperimentalStdlibApi::class)
private suspend fun redactedMessageViewModel(
timelineEvent: TimelineEvent,
redactedBy: UserId? = null,
coroutineContext: CoroutineContext,
): RedactedMessageViewModelImpl {
Dispatchers.setMain(checkNotNull(currentCoroutineContext()[CoroutineDispatcher]))
val di = koinApplication {
modules(
createTestDefaultTrixnityMessengerModules(mapOf(UserId("test", "server") to matrixClientMock))
)
}.koin
return RedactedMessageViewModelImpl(
viewModelContext = MatrixClientViewModelContextImpl(
componentContext = DefaultComponentContext(LifecycleRegistry()),
di = di,
userId = UserId("test", "server"),
coroutineContext = coroutineContext
),
timelineEvent = timelineEvent,
content = timelineEvent.event.content as RedactedEventContent,
formattedDate = "",
showDateAbove = false,
invitation = MutableStateFlow(""),
sender = MutableStateFlow(UserInfoElement("Bob")),
formattedTime = "",
isByMe = false,
redactedBy = redactedBy,
selectedRoomId = roomId,
showBigGap = false,
showSender = MutableStateFlow(false),
showChatBubbleEdge = false
)
}
private fun timelineEvent(
event: ClientEvent.RoomEvent<*>,
content: Result<RoomEventContent>? = null,
previousEvent: TimelineEvent? = null
): TimelineEvent {
val timelineEvent = TimelineEvent(
event = event,
content = content,
previousEventId = previousEvent?.eventId,
nextEventId = null,
gap = null,
)
mocker.every {
roomServiceMock.getPreviousTimelineEvent(
isTimelineEvent(timelineEvent),
isAny(),
)
} returns
previousEvent?.let { MutableStateFlow(it) }
return timelineEvent
}
private fun messageEvent(content: MessageEventContent, sender: UserId) = ClientEvent.RoomEvent.MessageEvent(
content,
id = EventId(""),
sender = sender,
roomId = roomId,
originTimestamp = 0L,
)
private fun roomUser(userId: UserId, name: String) = RoomUser(
roomId,
userId,
name,
event = ClientEvent.RoomEvent.StateEvent(
MemberEventContent(membership = Membership.JOIN),
EventId(""),
UserId(""),
RoomId(""),
0L,
stateKey = ""
)
)
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment