Skip to content

Commit f62f661

Browse files
author
Florian Renaud
committed
Room list - Do not show live broadcast if the started event is redacted
1 parent 493fa7a commit f62f661

File tree

5 files changed

+321
-36
lines changed

5 files changed

+321
-36
lines changed

vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,41 +22,33 @@ import com.airbnb.mvrx.Loading
2222
import im.vector.app.R
2323
import im.vector.app.core.date.DateFormatKind
2424
import im.vector.app.core.date.VectorDateFormatter
25-
import im.vector.app.core.di.ActiveSessionHolder
2625
import im.vector.app.core.epoxy.VectorEpoxyModel
2726
import im.vector.app.core.error.ErrorFormatter
2827
import im.vector.app.core.resources.StringProvider
2928
import im.vector.app.features.home.AvatarRenderer
3029
import im.vector.app.features.home.RoomListDisplayMode
3130
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
31+
import im.vector.app.features.home.room.list.usecase.GetLatestPreviewableEventUseCase
3232
import im.vector.app.features.home.room.typing.TypingHelper
3333
import im.vector.app.features.voicebroadcast.isLive
34-
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
3534
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
36-
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
3735
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
3836
import org.matrix.android.sdk.api.extensions.orFalse
39-
import org.matrix.android.sdk.api.session.events.model.EventType
40-
import org.matrix.android.sdk.api.session.getRoom
41-
import org.matrix.android.sdk.api.session.room.getTimelineEvent
4237
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
4338
import org.matrix.android.sdk.api.session.room.model.Membership
4439
import org.matrix.android.sdk.api.session.room.model.RoomSummary
4540
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
46-
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
47-
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
4841
import org.matrix.android.sdk.api.util.toMatrixItem
4942
import javax.inject.Inject
5043

5144
class RoomSummaryItemFactory @Inject constructor(
52-
private val sessionHolder: ActiveSessionHolder,
5345
private val displayableEventFormatter: DisplayableEventFormatter,
5446
private val dateFormatter: VectorDateFormatter,
5547
private val stringProvider: StringProvider,
5648
private val typingHelper: TypingHelper,
5749
private val avatarRenderer: AvatarRenderer,
5850
private val errorFormatter: ErrorFormatter,
59-
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
51+
private val getLatestPreviewableEventUseCase: GetLatestPreviewableEventUseCase,
6052
) {
6153

6254
fun create(
@@ -142,15 +134,16 @@ class RoomSummaryItemFactory @Inject constructor(
142134
val showSelected = selectedRoomIds.contains(roomSummary.roomId)
143135
var latestFormattedEvent: CharSequence = ""
144136
var latestEventTime = ""
145-
val latestEvent = roomSummary.getVectorLatestPreviewableEvent()
137+
val latestEvent = getLatestPreviewableEventUseCase.execute(roomSummary.roomId)
146138
if (latestEvent != null) {
147139
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
148140
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
149141
}
150142

151143
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
152144
// Skip typing while there is a live voice broadcast
153-
.takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }.orEmpty()
145+
.takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }
146+
.orEmpty()
154147

155148
return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) {
156149
createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick)
@@ -240,14 +233,4 @@ class RoomSummaryItemFactory @Inject constructor(
240233
else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1)
241234
}
242235
}
243-
244-
private fun RoomSummary.getVectorLatestPreviewableEvent(): TimelineEvent? {
245-
val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent
246-
val liveVoiceBroadcastTimelineEvent = getRoomLiveVoiceBroadcastsUseCase.execute(roomId).lastOrNull()
247-
?.root?.eventId?.let { room.getTimelineEvent(it) }
248-
return latestPreviewableEvent?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
249-
?: liveVoiceBroadcastTimelineEvent
250-
?: latestPreviewableEvent
251-
?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
252-
}
253236
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.home.room.list.usecase
18+
19+
import im.vector.app.core.di.ActiveSessionHolder
20+
import im.vector.app.features.voicebroadcast.isLive
21+
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
22+
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
23+
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
24+
import im.vector.app.features.voicebroadcast.voiceBroadcastId
25+
import org.matrix.android.sdk.api.extensions.orFalse
26+
import org.matrix.android.sdk.api.session.events.model.EventType
27+
import org.matrix.android.sdk.api.session.getRoom
28+
import org.matrix.android.sdk.api.session.room.Room
29+
import org.matrix.android.sdk.api.session.room.getTimelineEvent
30+
import org.matrix.android.sdk.api.session.room.model.RoomSummary
31+
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
32+
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
33+
import javax.inject.Inject
34+
35+
class GetLatestPreviewableEventUseCase @Inject constructor(
36+
private val sessionHolder: ActiveSessionHolder,
37+
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
38+
) {
39+
40+
fun execute(roomId: String): TimelineEvent? {
41+
val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return null
42+
val roomSummary = room.roomSummary() ?: return null
43+
return getCallEvent(roomSummary)
44+
?: getLiveVoiceBroadcastEvent(room)
45+
?: getDefaultLatestEvent(room, roomSummary)
46+
}
47+
48+
private fun getCallEvent(roomSummary: RoomSummary): TimelineEvent? {
49+
return roomSummary.latestPreviewableEvent
50+
?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
51+
}
52+
53+
private fun getLiveVoiceBroadcastEvent(room: Room): TimelineEvent? {
54+
return getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId)
55+
.lastOrNull()
56+
?.voiceBroadcastId
57+
?.let { room.getTimelineEvent(it) }
58+
}
59+
60+
private fun getDefaultLatestEvent(room: Room, roomSummary: RoomSummary): TimelineEvent? {
61+
val latestPreviewableEvent = roomSummary.latestPreviewableEvent
62+
63+
// If the default latest event is a live voice broadcast (paused or resumed), rely to the started event
64+
val liveVoiceBroadcastEventId = latestPreviewableEvent?.root?.asVoiceBroadcastEvent()?.takeIf { it.isLive }?.voiceBroadcastId
65+
if (liveVoiceBroadcastEventId != null) {
66+
return room.getTimelineEvent(liveVoiceBroadcastEventId)
67+
}
68+
69+
return latestPreviewableEvent
70+
?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
71+
}
72+
}

vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
2020
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
2121
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
2222
import im.vector.app.features.voicebroadcast.voiceBroadcastId
23+
import org.matrix.android.sdk.api.extensions.orTrue
2324
import org.matrix.android.sdk.api.session.Session
2425
import org.matrix.android.sdk.api.session.events.model.RelationType
2526
import org.matrix.android.sdk.api.session.getRoom
2627
import org.matrix.android.sdk.api.session.room.Room
28+
import org.matrix.android.sdk.api.session.room.getTimelineEvent
2729
import timber.log.Timber
2830
import javax.inject.Inject
2931

@@ -47,8 +49,14 @@ class GetVoiceBroadcastStateEventUseCase @Inject constructor(
4749
* Get the most recent event related to the given voice broadcast.
4850
*/
4951
private fun getMostRecentRelatedEvent(room: Room, voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? {
50-
return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
51-
.mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent()?.takeUnless { it.root.isRedacted() } }
52-
.maxByOrNull { it.root.originServerTs ?: 0 }
52+
val startedEvent = room.getTimelineEvent(voiceBroadcast.voiceBroadcastId)
53+
return if (startedEvent?.root?.isRedacted().orTrue()) {
54+
null
55+
} else {
56+
room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
57+
.mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent() }
58+
.filterNot { it.root.isRedacted() }
59+
.maxByOrNull { it.root.originServerTs ?: 0 }
60+
}
5361
}
5462
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.home.room.list.usecase
18+
19+
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
20+
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY
21+
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
22+
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
23+
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
24+
import im.vector.app.test.fakes.FakeActiveSessionHolder
25+
import im.vector.app.test.fakes.FakeRoom
26+
import io.mockk.every
27+
import io.mockk.mockk
28+
import org.amshove.kluent.shouldBe
29+
import org.amshove.kluent.shouldBeEqualTo
30+
import org.amshove.kluent.shouldBeNull
31+
import org.junit.Before
32+
import org.junit.Test
33+
import org.matrix.android.sdk.api.session.events.model.Event
34+
import org.matrix.android.sdk.api.session.events.model.EventType
35+
import org.matrix.android.sdk.api.session.events.model.RelationType
36+
import org.matrix.android.sdk.api.session.getRoom
37+
import org.matrix.android.sdk.api.session.room.model.RoomSummary
38+
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
39+
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
40+
41+
private const val A_ROOM_ID = "a-room-id"
42+
43+
internal class GetLatestPreviewableEventUseCaseTest {
44+
45+
private val fakeRoom = FakeRoom()
46+
private val fakeSessionHolder = FakeActiveSessionHolder()
47+
private val fakeRoomSummary = mockk<RoomSummary>()
48+
private val fakeGetRoomLiveVoiceBroadcastsUseCase = mockk<GetRoomLiveVoiceBroadcastsUseCase>()
49+
50+
private val getLatestPreviewableEventUseCase = GetLatestPreviewableEventUseCase(
51+
fakeSessionHolder.instance,
52+
fakeGetRoomLiveVoiceBroadcastsUseCase,
53+
)
54+
55+
@Before
56+
fun setup() {
57+
every { fakeSessionHolder.instance.getSafeActiveSession()?.getRoom(A_ROOM_ID) } returns fakeRoom
58+
every { fakeRoom.roomSummary() } returns fakeRoomSummary
59+
every { fakeRoom.roomId } returns A_ROOM_ID
60+
every { fakeRoom.timelineService().getTimelineEvent(any()) } answers {
61+
mockk(relaxed = true) {
62+
every { eventId } returns firstArg()
63+
}
64+
}
65+
}
66+
67+
@Test
68+
fun `given the latest event is a call invite and there is a live broadcast, when execute, returns the call event`() {
69+
// Given
70+
val aLatestPreviewableEvent = mockk<TimelineEvent> {
71+
every { root.type } returns EventType.MESSAGE
72+
every { root.getClearType() } returns EventType.CALL_INVITE
73+
}
74+
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
75+
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns listOf(
76+
givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STARTED, "id1"),
77+
givenAVoiceBroadcastEvent("id2", VoiceBroadcastState.RESUMED, "id1"),
78+
).mapNotNull { it.asVoiceBroadcastEvent() }
79+
80+
// When
81+
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
82+
83+
// Then
84+
result shouldBe aLatestPreviewableEvent
85+
}
86+
87+
@Test
88+
fun `given the latest event is not a call invite and there is a live broadcast, when execute, returns the latest broadcast event`() {
89+
// Given
90+
val aLatestPreviewableEvent = mockk<TimelineEvent> {
91+
every { root.type } returns EventType.MESSAGE
92+
every { root.getClearType() } returns EventType.MESSAGE
93+
}
94+
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
95+
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns listOf(
96+
givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STARTED, "vb_id1"),
97+
givenAVoiceBroadcastEvent("id2", VoiceBroadcastState.RESUMED, "vb_id2"),
98+
).mapNotNull { it.asVoiceBroadcastEvent() }
99+
100+
// When
101+
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
102+
103+
// Then
104+
result?.eventId shouldBeEqualTo "vb_id2"
105+
}
106+
107+
@Test
108+
fun `given there is no live broadcast, when execute, returns the latest event`() {
109+
// Given
110+
val aLatestPreviewableEvent = mockk<TimelineEvent> {
111+
every { root.type } returns EventType.MESSAGE
112+
every { root.getClearType() } returns EventType.MESSAGE
113+
}
114+
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
115+
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
116+
117+
// When
118+
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
119+
120+
// Then
121+
result shouldBe aLatestPreviewableEvent
122+
}
123+
124+
@Test
125+
fun `given there is no live broadcast and the latest event is a vb message, when execute, returns null`() {
126+
// Given
127+
val aLatestPreviewableEvent = mockk<TimelineEvent> {
128+
every { root.type } returns EventType.MESSAGE
129+
every { root.getClearType() } returns EventType.MESSAGE
130+
every { root.getClearContent() } returns mapOf(
131+
MessageContent.MSG_TYPE_JSON_KEY to "m.audio",
132+
VOICE_BROADCAST_CHUNK_KEY to "1",
133+
"body" to "",
134+
)
135+
}
136+
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
137+
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
138+
139+
// When
140+
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
141+
142+
// Then
143+
result.shouldBeNull()
144+
}
145+
146+
@Test
147+
fun `given the latest event is an ended vb, when execute, returns the stopped event`() {
148+
// Given
149+
val aLatestPreviewableEvent = mockk<TimelineEvent> {
150+
every { eventId } returns "id1"
151+
every { root } returns givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STOPPED, "vb_id1")
152+
}
153+
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
154+
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
155+
156+
// When
157+
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
158+
159+
// Then
160+
result?.eventId shouldBeEqualTo "id1"
161+
}
162+
163+
@Test
164+
fun `given the latest event is a resumed vb, when execute, returns the started event`() {
165+
// Given
166+
val aLatestPreviewableEvent = mockk<TimelineEvent> {
167+
every { eventId } returns "id1"
168+
every { root } returns givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.RESUMED, "vb_id1")
169+
}
170+
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
171+
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
172+
173+
// When
174+
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
175+
176+
// Then
177+
result?.eventId shouldBeEqualTo "vb_id1"
178+
}
179+
180+
private fun givenAVoiceBroadcastEvent(
181+
eventId: String,
182+
state: VoiceBroadcastState,
183+
voiceBroadcastId: String,
184+
): Event = mockk {
185+
every { this@mockk.eventId } returns eventId
186+
every { getClearType() } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
187+
every { type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
188+
every { content } returns mapOf(
189+
"state" to state.value,
190+
"m.relates_to" to mapOf(
191+
"rel_type" to RelationType.REFERENCE,
192+
"event_id" to voiceBroadcastId
193+
)
194+
)
195+
}
196+
}

0 commit comments

Comments
 (0)