Room list - Do not show live broadcast if the started event is redacted

This commit is contained in:
Florian Renaud 2023-01-11 17:21:34 +01:00
parent 493fa7a0eb
commit f62f661d2b
5 changed files with 321 additions and 36 deletions

View File

@ -22,41 +22,33 @@ import com.airbnb.mvrx.Loading
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
import im.vector.app.features.home.room.list.usecase.GetLatestPreviewableEventUseCase
import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.voicebroadcast.isLive
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class RoomSummaryItemFactory @Inject constructor(
private val sessionHolder: ActiveSessionHolder,
private val displayableEventFormatter: DisplayableEventFormatter,
private val dateFormatter: VectorDateFormatter,
private val stringProvider: StringProvider,
private val typingHelper: TypingHelper,
private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter,
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
private val getLatestPreviewableEventUseCase: GetLatestPreviewableEventUseCase,
) {
fun create(
@ -142,7 +134,7 @@ class RoomSummaryItemFactory @Inject constructor(
val showSelected = selectedRoomIds.contains(roomSummary.roomId)
var latestFormattedEvent: CharSequence = ""
var latestEventTime = ""
val latestEvent = roomSummary.getVectorLatestPreviewableEvent()
val latestEvent = getLatestPreviewableEventUseCase.execute(roomSummary.roomId)
if (latestEvent != null) {
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
@ -150,7 +142,8 @@ class RoomSummaryItemFactory @Inject constructor(
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
// Skip typing while there is a live voice broadcast
.takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }.orEmpty()
.takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }
.orEmpty()
return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) {
createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick)
@ -240,14 +233,4 @@ class RoomSummaryItemFactory @Inject constructor(
else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1)
}
}
private fun RoomSummary.getVectorLatestPreviewableEvent(): TimelineEvent? {
val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent
val liveVoiceBroadcastTimelineEvent = getRoomLiveVoiceBroadcastsUseCase.execute(roomId).lastOrNull()
?.root?.eventId?.let { room.getTimelineEvent(it) }
return latestPreviewableEvent?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
?: liveVoiceBroadcastTimelineEvent
?: latestPreviewableEvent
?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
}
}

View File

@ -0,0 +1,72 @@
/*
* 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 im.vector.app.features.home.room.list.usecase
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.voicebroadcast.isLive
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
import im.vector.app.features.voicebroadcast.voiceBroadcastId
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
class GetLatestPreviewableEventUseCase @Inject constructor(
private val sessionHolder: ActiveSessionHolder,
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
) {
fun execute(roomId: String): TimelineEvent? {
val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return null
val roomSummary = room.roomSummary() ?: return null
return getCallEvent(roomSummary)
?: getLiveVoiceBroadcastEvent(room)
?: getDefaultLatestEvent(room, roomSummary)
}
private fun getCallEvent(roomSummary: RoomSummary): TimelineEvent? {
return roomSummary.latestPreviewableEvent
?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
}
private fun getLiveVoiceBroadcastEvent(room: Room): TimelineEvent? {
return getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId)
.lastOrNull()
?.voiceBroadcastId
?.let { room.getTimelineEvent(it) }
}
private fun getDefaultLatestEvent(room: Room, roomSummary: RoomSummary): TimelineEvent? {
val latestPreviewableEvent = roomSummary.latestPreviewableEvent
// If the default latest event is a live voice broadcast (paused or resumed), rely to the started event
val liveVoiceBroadcastEventId = latestPreviewableEvent?.root?.asVoiceBroadcastEvent()?.takeIf { it.isLive }?.voiceBroadcastId
if (liveVoiceBroadcastEventId != null) {
return room.getTimelineEvent(liveVoiceBroadcastEventId)
}
return latestPreviewableEvent
?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
}
}

View File

@ -20,10 +20,12 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.voiceBroadcastId
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import timber.log.Timber
import javax.inject.Inject
@ -47,8 +49,14 @@ class GetVoiceBroadcastStateEventUseCase @Inject constructor(
* Get the most recent event related to the given voice broadcast.
*/
private fun getMostRecentRelatedEvent(room: Room, voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? {
return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
.mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent()?.takeUnless { it.root.isRedacted() } }
.maxByOrNull { it.root.originServerTs ?: 0 }
val startedEvent = room.getTimelineEvent(voiceBroadcast.voiceBroadcastId)
return if (startedEvent?.root?.isRedacted().orTrue()) {
null
} else {
room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
.mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent() }
.filterNot { it.root.isRedacted() }
.maxByOrNull { it.root.originServerTs ?: 0 }
}
}
}

View File

@ -0,0 +1,196 @@
/*
* 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 im.vector.app.features.home.room.list.usecase
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeRoom
import io.mockk.every
import io.mockk.mockk
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeNull
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
private const val A_ROOM_ID = "a-room-id"
internal class GetLatestPreviewableEventUseCaseTest {
private val fakeRoom = FakeRoom()
private val fakeSessionHolder = FakeActiveSessionHolder()
private val fakeRoomSummary = mockk<RoomSummary>()
private val fakeGetRoomLiveVoiceBroadcastsUseCase = mockk<GetRoomLiveVoiceBroadcastsUseCase>()
private val getLatestPreviewableEventUseCase = GetLatestPreviewableEventUseCase(
fakeSessionHolder.instance,
fakeGetRoomLiveVoiceBroadcastsUseCase,
)
@Before
fun setup() {
every { fakeSessionHolder.instance.getSafeActiveSession()?.getRoom(A_ROOM_ID) } returns fakeRoom
every { fakeRoom.roomSummary() } returns fakeRoomSummary
every { fakeRoom.roomId } returns A_ROOM_ID
every { fakeRoom.timelineService().getTimelineEvent(any()) } answers {
mockk(relaxed = true) {
every { eventId } returns firstArg()
}
}
}
@Test
fun `given the latest event is a call invite and there is a live broadcast, when execute, returns the call event`() {
// Given
val aLatestPreviewableEvent = mockk<TimelineEvent> {
every { root.type } returns EventType.MESSAGE
every { root.getClearType() } returns EventType.CALL_INVITE
}
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns listOf(
givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STARTED, "id1"),
givenAVoiceBroadcastEvent("id2", VoiceBroadcastState.RESUMED, "id1"),
).mapNotNull { it.asVoiceBroadcastEvent() }
// When
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
// Then
result shouldBe aLatestPreviewableEvent
}
@Test
fun `given the latest event is not a call invite and there is a live broadcast, when execute, returns the latest broadcast event`() {
// Given
val aLatestPreviewableEvent = mockk<TimelineEvent> {
every { root.type } returns EventType.MESSAGE
every { root.getClearType() } returns EventType.MESSAGE
}
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns listOf(
givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STARTED, "vb_id1"),
givenAVoiceBroadcastEvent("id2", VoiceBroadcastState.RESUMED, "vb_id2"),
).mapNotNull { it.asVoiceBroadcastEvent() }
// When
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
// Then
result?.eventId shouldBeEqualTo "vb_id2"
}
@Test
fun `given there is no live broadcast, when execute, returns the latest event`() {
// Given
val aLatestPreviewableEvent = mockk<TimelineEvent> {
every { root.type } returns EventType.MESSAGE
every { root.getClearType() } returns EventType.MESSAGE
}
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
// When
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
// Then
result shouldBe aLatestPreviewableEvent
}
@Test
fun `given there is no live broadcast and the latest event is a vb message, when execute, returns null`() {
// Given
val aLatestPreviewableEvent = mockk<TimelineEvent> {
every { root.type } returns EventType.MESSAGE
every { root.getClearType() } returns EventType.MESSAGE
every { root.getClearContent() } returns mapOf(
MessageContent.MSG_TYPE_JSON_KEY to "m.audio",
VOICE_BROADCAST_CHUNK_KEY to "1",
"body" to "",
)
}
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
// When
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
// Then
result.shouldBeNull()
}
@Test
fun `given the latest event is an ended vb, when execute, returns the stopped event`() {
// Given
val aLatestPreviewableEvent = mockk<TimelineEvent> {
every { eventId } returns "id1"
every { root } returns givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STOPPED, "vb_id1")
}
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
// When
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
// Then
result?.eventId shouldBeEqualTo "id1"
}
@Test
fun `given the latest event is a resumed vb, when execute, returns the started event`() {
// Given
val aLatestPreviewableEvent = mockk<TimelineEvent> {
every { eventId } returns "id1"
every { root } returns givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.RESUMED, "vb_id1")
}
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
// When
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
// Then
result?.eventId shouldBeEqualTo "vb_id1"
}
private fun givenAVoiceBroadcastEvent(
eventId: String,
state: VoiceBroadcastState,
voiceBroadcastId: String,
): Event = mockk {
every { this@mockk.eventId } returns eventId
every { getClearType() } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
every { type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
every { content } returns mapOf(
"state" to state.value,
"m.relates_to" to mapOf(
"rel_type" to RelationType.REFERENCE,
"event_id" to voiceBroadcastId
)
)
}
}

View File

@ -18,6 +18,7 @@ package im.vector.app.features.voicebroadcast.usecase
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.test.fakes.FakeSession
import io.mockk.every
import io.mockk.mockk
@ -40,6 +41,7 @@ internal class GetVoiceBroadcastStateEventUseCaseTest {
fun `given there is no event related to the given vb, when execute, then return null`() {
// Given
val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEvent(A_VOICE_BROADCAST_ID) } returns null
every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns emptyList()
// When
@ -54,9 +56,9 @@ internal class GetVoiceBroadcastStateEventUseCaseTest {
// Given
val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
val aListOfTimelineEvents = listOf(
givenAVoiceBroadcastEvent(eventId = "event_id_1", isRedacted = false, timestamp = 1L),
givenAVoiceBroadcastEvent(eventId = "event_id_3", isRedacted = false, timestamp = 3L),
givenAVoiceBroadcastEvent(eventId = "event_id_2", isRedacted = false, timestamp = 2L),
givenAVoiceBroadcastEvent(eventId = A_VOICE_BROADCAST_ID, state = VoiceBroadcastState.STARTED, isRedacted = false, timestamp = 1L),
givenAVoiceBroadcastEvent(eventId = "event_id_3", state = VoiceBroadcastState.STOPPED, isRedacted = false, timestamp = 3L),
givenAVoiceBroadcastEvent(eventId = "event_id_2", state = VoiceBroadcastState.PAUSED, isRedacted = false, timestamp = 2L),
)
every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents
@ -73,8 +75,8 @@ internal class GetVoiceBroadcastStateEventUseCaseTest {
// Given
val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
val aListOfTimelineEvents = listOf(
givenAVoiceBroadcastEvent(eventId = "event_id_1", isRedacted = false, timestamp = 1L),
givenAVoiceBroadcastEvent(eventId = "event_id_2", isRedacted = true, timestamp = 2L),
givenAVoiceBroadcastEvent(eventId = A_VOICE_BROADCAST_ID, state = VoiceBroadcastState.STARTED, isRedacted = false, timestamp = 1L),
givenAVoiceBroadcastEvent(eventId = "event_id_2", state = VoiceBroadcastState.STOPPED, isRedacted = true, timestamp = 2L),
)
every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents
@ -83,17 +85,41 @@ internal class GetVoiceBroadcastStateEventUseCaseTest {
// Then
result.shouldNotBeNull()
result.root.eventId shouldBeEqualTo "event_id_1"
result.root.eventId shouldBeEqualTo A_VOICE_BROADCAST_ID
}
@Test
fun `given a not ended voice broadcast with a redacted start event, when execute, then return null`() {
// Given
val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
val aListOfTimelineEvents = listOf(
givenAVoiceBroadcastEvent(eventId = A_VOICE_BROADCAST_ID, state = VoiceBroadcastState.STARTED, isRedacted = true, timestamp = 1L),
givenAVoiceBroadcastEvent(eventId = "event_id_2", state = VoiceBroadcastState.PAUSED, isRedacted = false, timestamp = 2L),
givenAVoiceBroadcastEvent(eventId = "event_id_3", state = VoiceBroadcastState.RESUMED, isRedacted = false, timestamp = 3L),
)
every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents
// When
val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast)
// Then
result.shouldBeNull()
}
private fun givenAVoiceBroadcastEvent(
eventId: String,
state: VoiceBroadcastState,
isRedacted: Boolean,
timestamp: Long,
) = mockk<TimelineEvent>(relaxed = true) {
every { root.eventId } returns eventId
every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
every { root.isRedacted() } returns isRedacted
every { root.originServerTs } returns timestamp
): TimelineEvent {
val timelineEvent = mockk<TimelineEvent> {
every { root.eventId } returns eventId
every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
every { root.content } returns mapOf("state" to state.value)
every { root.isRedacted() } returns isRedacted
every { root.originServerTs } returns timestamp
}
every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEvent(eventId) } returns timelineEvent
return timelineEvent
}
}