Merge pull request #7898 from vector-im/bugfix/fre/unexpected_live_vb_room_list

Fix unexpected live voice broadcast in the room list
This commit is contained in:
Florian Renaud 2023-01-12 10:42:52 +01:00 committed by GitHub
commit b1d2581bf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 471 additions and 34 deletions

1
changelog.d/7832.bugfix Normal file
View File

@ -0,0 +1 @@
[Voice Broadcast] Fix unexpected "live broadcast" in the room list

View File

@ -22,41 +22,33 @@ import com.airbnb.mvrx.Loading
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter 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.epoxy.VectorEpoxyModel
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.RoomListDisplayMode 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.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.home.room.typing.TypingHelper
import im.vector.app.features.voicebroadcast.isLive 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.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse 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.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership 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.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo 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 org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
class RoomSummaryItemFactory @Inject constructor( class RoomSummaryItemFactory @Inject constructor(
private val sessionHolder: ActiveSessionHolder,
private val displayableEventFormatter: DisplayableEventFormatter, private val displayableEventFormatter: DisplayableEventFormatter,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val typingHelper: TypingHelper, private val typingHelper: TypingHelper,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter, private val errorFormatter: ErrorFormatter,
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase, private val getLatestPreviewableEventUseCase: GetLatestPreviewableEventUseCase,
) { ) {
fun create( fun create(
@ -142,7 +134,7 @@ class RoomSummaryItemFactory @Inject constructor(
val showSelected = selectedRoomIds.contains(roomSummary.roomId) val showSelected = selectedRoomIds.contains(roomSummary.roomId)
var latestFormattedEvent: CharSequence = "" var latestFormattedEvent: CharSequence = ""
var latestEventTime = "" var latestEventTime = ""
val latestEvent = roomSummary.getVectorLatestPreviewableEvent() val latestEvent = getLatestPreviewableEventUseCase.execute(roomSummary.roomId)
if (latestEvent != null) { if (latestEvent != null) {
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not()) latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST) latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
@ -150,7 +142,8 @@ class RoomSummaryItemFactory @Inject constructor(
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers) val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
// Skip typing while there is a live voice broadcast // 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) { return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) {
createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick) 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) 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

@ -19,14 +19,20 @@ package im.vector.app.features.voicebroadcast.usecase
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.isLive import im.vector.app.features.voicebroadcast.isLive
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.voiceBroadcastId
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import javax.inject.Inject import javax.inject.Inject
/**
* Get the list of live (not ended) voice broadcast events in the given room.
*/
class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor( class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val getVoiceBroadcastStateEventUseCase: GetVoiceBroadcastStateEventUseCase,
) { ) {
fun execute(roomId: String): List<VoiceBroadcastEvent> { fun execute(roomId: String): List<VoiceBroadcastEvent> {
@ -37,7 +43,8 @@ class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor(
setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO), setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO),
QueryStringValue.IsNotEmpty QueryStringValue.IsNotEmpty
) )
.mapNotNull { it.asVoiceBroadcastEvent() } .mapNotNull { stateEvent -> stateEvent.asVoiceBroadcastEvent()?.voiceBroadcastId }
.mapNotNull { voiceBroadcastId -> getVoiceBroadcastStateEventUseCase.execute(VoiceBroadcast(voiceBroadcastId, roomId)) }
.filter { it.isLive } .filter { it.isLive }
} }
} }

View File

@ -32,7 +32,6 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.flow.transformWhile
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session 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.getRoom
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
@ -44,6 +43,7 @@ import javax.inject.Inject
class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor( class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
private val session: Session, private val session: Session,
private val getVoiceBroadcastStateEventUseCase: GetVoiceBroadcastStateEventUseCase,
) { ) {
fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> { fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
@ -93,7 +93,7 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
* Get a flow of the most recent related event. * Get a flow of the most recent related event.
*/ */
private fun getMostRecentRelatedEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> { private fun getMostRecentRelatedEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
val mostRecentEvent = getMostRecentRelatedEvent(room, voiceBroadcast).toOptional() val mostRecentEvent = getVoiceBroadcastStateEventUseCase.execute(voiceBroadcast).toOptional()
return if (mostRecentEvent.hasValue()) { return if (mostRecentEvent.hasValue()) {
val stateKey = mostRecentEvent.get().root.stateKey.orEmpty() val stateKey = mostRecentEvent.get().root.stateKey.orEmpty()
// observe incoming voice broadcast state events // observe incoming voice broadcast state events
@ -141,15 +141,6 @@ class GetVoiceBroadcastStateEventLiveUseCase @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 }
}
/** /**
* Get a flow of the given voice broadcast event changes. * Get a flow of the given voice broadcast event changes.
*/ */

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2022 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.voicebroadcast.usecase
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
class GetVoiceBroadcastStateEventUseCase @Inject constructor(
private val session: Session,
) {
fun execute(voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? {
val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}")
return getMostRecentRelatedEvent(room, voiceBroadcast)
.also { event ->
Timber.d(
"## VoiceBroadcast | " +
"voiceBroadcastId=${event?.voiceBroadcastId}, " +
"state=${event?.content?.voiceBroadcastState}"
)
}
}
/**
* Get the most recent event related to the given voice broadcast.
*/
private fun getMostRecentRelatedEvent(room: Room, voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? {
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

@ -0,0 +1,125 @@
/*
* 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.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
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldNotBeNull
import org.junit.Test
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
private const val A_ROOM_ID = "A_ROOM_ID"
private const val A_VOICE_BROADCAST_ID = "A_VOICE_BROADCAST_ID"
internal class GetVoiceBroadcastStateEventUseCaseTest {
private val fakeSession = FakeSession()
private val getVoiceBroadcastStateEventUseCase = GetVoiceBroadcastStateEventUseCase(fakeSession)
@Test
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
val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast)
// Then
result.shouldBeNull()
}
@Test
fun `given there are several related events related to the given vb, when execute, then return the most recent one`() {
// Given
val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
val aListOfTimelineEvents = listOf(
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
// When
val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast)
// Then
result.shouldNotBeNull()
result.root.eventId shouldBeEqualTo "event_id_3"
}
@Test
fun `given there are several related events related to the given vb, when execute, then return the most recent one which is not redacted`() {
// Given
val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
val aListOfTimelineEvents = listOf(
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
// When
val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast)
// Then
result.shouldNotBeNull()
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,
): 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
}
}