diff --git a/changelog.d/7832.bugfix b/changelog.d/7832.bugfix new file mode 100644 index 0000000000..871f9aabb9 --- /dev/null +++ b/changelog.d/7832.bugfix @@ -0,0 +1 @@ +[Voice Broadcast] Fix unexpected "live broadcast" in the room list diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index a55900a5c4..18c8ea3bde 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -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 - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt new file mode 100644 index 0000000000..6a50e87562 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt @@ -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 + } +} diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt index fa5f06bfe6..fb48328305 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt @@ -19,14 +19,20 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants 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.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.voiceBroadcastId import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.getRoom import javax.inject.Inject +/** + * Get the list of live (not ended) voice broadcast events in the given room. + */ class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, + private val getVoiceBroadcastStateEventUseCase: GetVoiceBroadcastStateEventUseCase, ) { fun execute(roomId: String): List { @@ -37,7 +43,8 @@ class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor( setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO), QueryStringValue.IsNotEmpty ) - .mapNotNull { it.asVoiceBroadcastEvent() } + .mapNotNull { stateEvent -> stateEvent.asVoiceBroadcastEvent()?.voiceBroadcastId } + .mapNotNull { voiceBroadcastId -> getVoiceBroadcastStateEventUseCase.execute(VoiceBroadcast(voiceBroadcastId, roomId)) } .filter { it.isLive } } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt index b3bbdad635..22fb0df6f9 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.transformWhile import org.matrix.android.sdk.api.query.QueryStringValue 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.util.Optional @@ -44,6 +43,7 @@ import javax.inject.Inject class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor( private val session: Session, + private val getVoiceBroadcastStateEventUseCase: GetVoiceBroadcastStateEventUseCase, ) { fun execute(voiceBroadcast: VoiceBroadcast): Flow> { @@ -93,7 +93,7 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor( * Get a flow of the most recent related event. */ private fun getMostRecentRelatedEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow> { - val mostRecentEvent = getMostRecentRelatedEvent(room, voiceBroadcast).toOptional() + val mostRecentEvent = getVoiceBroadcastStateEventUseCase.execute(voiceBroadcast).toOptional() return if (mostRecentEvent.hasValue()) { val stateKey = mostRecentEvent.get().root.stateKey.orEmpty() // 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. */ diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt new file mode 100644 index 0000000000..e821e09119 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt @@ -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 } + } + } +} diff --git a/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt new file mode 100644 index 0000000000..5d526c783b --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt @@ -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() + private val fakeGetRoomLiveVoiceBroadcastsUseCase = mockk() + + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 + ) + ) + } +} diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt new file mode 100644 index 0000000000..00b04aea81 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt @@ -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 { + 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 + } +}