Skip to content

Commit 9b9d75a

Browse files
authored
Add ActiveRoomsHolder to manage the active rooms for a session (#4758)
1 parent 630e1d1 commit 9b9d75a

19 files changed

Lines changed: 240 additions & 30 deletions

File tree

appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
3636
import io.element.android.libraries.matrix.api.core.UserId
3737
import io.element.android.libraries.matrix.api.permalink.PermalinkData
3838
import io.element.android.libraries.matrix.api.room.JoinedRoom
39+
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
3940
import io.element.android.services.appnavstate.api.AppNavigationStateService
4041
import kotlinx.coroutines.CoroutineScope
4142
import kotlinx.coroutines.launch
@@ -51,6 +52,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
5152
private val appNavigationStateService: AppNavigationStateService,
5253
private val appCoroutineScope: CoroutineScope,
5354
private val matrixClient: MatrixClient,
55+
private val activeRoomsHolder: ActiveRoomsHolder,
5456
roomComponentFactory: RoomComponentFactory,
5557
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
5658
backstack = BackStack(
@@ -85,6 +87,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
8587
onCreate = {
8688
Timber.v("OnCreate => ${inputs.room.roomId}")
8789
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
90+
activeRoomsHolder.addRoom(inputs.room)
8891
fetchRoomMembers()
8992
trackVisitedRoom()
9093
},
@@ -95,6 +98,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
9598
},
9699
onDestroy = {
97100
Timber.v("OnDestroy")
101+
activeRoomsHolder.removeRoom(inputs.room.sessionId, inputs.room.roomId)
98102
inputs.room.destroy()
99103
appNavigationStateService.onLeavingRoom(id)
100104
}

appnav/src/test/kotlin/io/element/android/appnav/JoinBaseRoomLoadedFlowNodeTest.kt renamed to appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ import io.element.android.features.messages.api.MessagesEntryPoint
2424
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
2525
import io.element.android.libraries.architecture.childNode
2626
import io.element.android.libraries.matrix.api.room.JoinedRoom
27+
import io.element.android.libraries.matrix.test.A_SESSION_ID
2728
import io.element.android.libraries.matrix.test.FakeMatrixClient
2829
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
2930
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
31+
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
3032
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
3133
import kotlinx.coroutines.CoroutineScope
3234
import kotlinx.coroutines.test.runTest
3335
import org.junit.Rule
3436
import org.junit.Test
3537

36-
class JoinBaseRoomLoadedFlowNodeTest {
38+
class JoinedRoomLoadedFlowNodeTest {
3739
@get:Rule
3840
val instantTaskExecutorRule = InstantTaskExecutorRule()
3941

@@ -100,6 +102,7 @@ class JoinBaseRoomLoadedFlowNodeTest {
100102
plugins: List<Plugin>,
101103
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
102104
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
105+
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
103106
coroutineScope: CoroutineScope,
104107
) = JoinedRoomLoadedFlowNode(
105108
buildContext = BuildContext.root(savedStateMap = null),
@@ -110,6 +113,7 @@ class JoinBaseRoomLoadedFlowNodeTest {
110113
appCoroutineScope = coroutineScope,
111114
roomComponentFactory = FakeRoomComponentFactory(),
112115
matrixClient = FakeMatrixClient(),
116+
activeRoomsHolder = activeRoomsHolder,
113117
)
114118

115119
@Test
@@ -154,4 +158,55 @@ class JoinBaseRoomLoadedFlowNodeTest {
154158
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
155159
assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
156160
}
161+
162+
@Test
163+
fun `the ActiveRoomsHolder will be updated with the loaded room on create`() = runTest {
164+
// GIVEN
165+
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
166+
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
167+
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
168+
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
169+
val activeRoomsHolder = ActiveRoomsHolder()
170+
val roomFlowNode = createJoinedRoomLoadedFlowNode(
171+
plugins = listOf(inputs),
172+
messagesEntryPoint = fakeMessagesEntryPoint,
173+
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
174+
coroutineScope = this,
175+
activeRoomsHolder = activeRoomsHolder,
176+
)
177+
178+
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
179+
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
180+
// WHEN
181+
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.CREATED)
182+
// THEN
183+
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNotNull()
184+
}
185+
186+
@Test
187+
fun `the ActiveRoomsHolder will be removed on destroy`() = runTest {
188+
// GIVEN
189+
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
190+
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
191+
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
192+
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
193+
val activeRoomsHolder = ActiveRoomsHolder().apply {
194+
addRoom(room)
195+
}
196+
val roomFlowNode = createJoinedRoomLoadedFlowNode(
197+
plugins = listOf(inputs),
198+
messagesEntryPoint = fakeMessagesEntryPoint,
199+
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
200+
coroutineScope = this,
201+
activeRoomsHolder = activeRoomsHolder,
202+
)
203+
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
204+
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.CREATED)
205+
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNotNull()
206+
// WHEN
207+
roomFlowNode.updateLifecycleState(Lifecycle.State.DESTROYED)
208+
// THEN
209+
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.DESTROYED)
210+
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
211+
}
157212
}

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.sync.SyncState
3939
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
4040
import io.element.android.libraries.network.useragent.UserAgentProvider
4141
import io.element.android.services.analytics.api.ScreenTracker
42+
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
4243
import io.element.android.services.appnavstate.api.AppForegroundStateService
4344
import io.element.android.services.toolbox.api.systemclock.SystemClock
4445
import kotlinx.coroutines.CoroutineScope
@@ -62,6 +63,7 @@ class CallScreenPresenter @AssistedInject constructor(
6263
private val activeCallManager: ActiveCallManager,
6364
private val languageTagProvider: LanguageTagProvider,
6465
private val appForegroundStateService: AppForegroundStateService,
66+
private val activeRoomsHolder: ActiveRoomsHolder,
6567
private val appCoroutineScope: CoroutineScope,
6668
) : Presenter<CallScreenState> {
6769
@AssistedFactory
@@ -241,8 +243,10 @@ class CallScreenPresenter @AssistedInject constructor(
241243

242244
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
243245
if (!notifiedCallStart) {
244-
getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
245-
?.onSuccess { notifiedCallStart = true }
246+
val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
247+
val sendCallNotificationResult = activeRoomForSession?.sendCallNotificationIfNeeded()
248+
?: getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
249+
sendCallNotificationResult?.onSuccess { notifiedCallStart = true }
246250
}
247251
}
248252

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
1414
import io.element.android.libraries.matrix.api.core.SessionId
1515
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
1616
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
17+
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
1718
import kotlinx.coroutines.flow.firstOrNull
1819
import javax.inject.Inject
1920

@@ -24,6 +25,7 @@ class DefaultCallWidgetProvider @Inject constructor(
2425
private val matrixClientsProvider: MatrixClientProvider,
2526
private val appPreferencesStore: AppPreferencesStore,
2627
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
28+
private val activeRoomsHolder: ActiveRoomsHolder,
2729
) : CallWidgetProvider {
2830
override suspend fun getWidget(
2931
sessionId: SessionId,
@@ -33,7 +35,9 @@ class DefaultCallWidgetProvider @Inject constructor(
3335
theme: String?,
3436
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
3537
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
36-
val room = matrixClient.getJoinedRoom(roomId) ?: error("Room not found")
38+
val room = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
39+
?: matrixClient.getJoinedRoom(roomId)
40+
?: error("Room not found")
3741

3842
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
3943
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL

features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
3232
import io.element.android.libraries.network.useragent.UserAgentProvider
3333
import io.element.android.services.analytics.api.ScreenTracker
3434
import io.element.android.services.analytics.test.FakeScreenTracker
35+
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
3536
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
3637
import io.element.android.services.toolbox.api.systemclock.SystemClock
3738
import io.element.android.tests.testutils.WarmUpRule
@@ -367,6 +368,7 @@ import kotlin.time.Duration.Companion.seconds
367368
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
368369
screenTracker: ScreenTracker = FakeScreenTracker(),
369370
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
371+
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
370372
): CallScreenPresenter {
371373
val userAgentProvider = object : UserAgentProvider {
372374
override fun provide(): String {
@@ -387,6 +389,7 @@ import kotlin.time.Duration.Companion.seconds
387389
languageTagProvider = FakeLanguageTagProvider("en-US"),
388390
appForegroundStateService = appForegroundStateService,
389391
appCoroutineScope = backgroundScope,
392+
activeRoomsHolder = activeRoomsHolder,
390393
)
391394
}
392395
}

features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
1515
import io.element.android.libraries.matrix.test.A_SESSION_ID
1616
import io.element.android.libraries.matrix.test.FakeMatrixClient
1717
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
18+
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
1819
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
1920
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
2021
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
2122
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
2223
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
24+
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
2325
import kotlinx.coroutines.test.runTest
2426
import org.junit.Test
2527

@@ -77,6 +79,23 @@ class DefaultCallWidgetProviderTest {
7779
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull()
7880
}
7981

82+
@Test
83+
fun `getWidget - reuses the active room if possible`() = runTest {
84+
val client = FakeMatrixClient().apply {
85+
// No room from the client
86+
givenGetRoomResult(A_ROOM_ID, null)
87+
}
88+
val activeRoomsHolder = ActiveRoomsHolder().apply {
89+
// A current active room with the same room id
90+
addRoom(FakeJoinedRoom(baseRoom = FakeBaseRoom(roomId = A_ROOM_ID)))
91+
}
92+
val provider = createProvider(
93+
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
94+
activeRoomsHolder = activeRoomsHolder
95+
)
96+
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue()
97+
}
98+
8099
@Test
81100
fun `getWidget - will use a custom base url if it exists`() = runTest {
82101
val room = FakeJoinedRoom(
@@ -104,9 +123,11 @@ class DefaultCallWidgetProviderTest {
104123
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
105124
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
106125
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
126+
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
107127
) = DefaultCallWidgetProvider(
108128
matrixClientsProvider = matrixClientProvider,
109129
appPreferencesStore = appPreferencesStore,
110130
callWidgetSettingsProvider = callWidgetSettingsProvider,
131+
activeRoomsHolder = activeRoomsHolder,
111132
)
112133
}

features/preferences/impl/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ dependencies {
7878
implementation(projects.features.roomlist.api)
7979
implementation(projects.services.analytics.api)
8080
implementation(projects.services.analytics.compose)
81+
implementation(projects.services.appnavstate.api)
8182
implementation(projects.services.toolbox.api)
8283
implementation(libs.datetime)
8384
implementation(libs.coil.compose)

features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import io.element.android.libraries.di.ApplicationContext
1818
import io.element.android.libraries.di.SessionScope
1919
import io.element.android.libraries.matrix.api.MatrixClient
2020
import io.element.android.libraries.push.api.PushService
21+
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
2122
import kotlinx.coroutines.withContext
2223
import okhttp3.OkHttpClient
2324
import javax.inject.Inject
@@ -37,8 +38,11 @@ class DefaultClearCacheUseCase @Inject constructor(
3738
private val ftueService: FtueService,
3839
private val pushService: PushService,
3940
private val seenInvitesStore: SeenInvitesStore,
41+
private val activeRoomsHolder: ActiveRoomsHolder,
4042
) : ClearCacheUseCase {
4143
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
44+
// Active rooms should be disposed of before clearing the cache
45+
activeRoomsHolder.clear(matrixClient.sessionId)
4246
// Clear Matrix cache
4347
matrixClient.clearCache()
4448
// Clear Coil cache

features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ import io.element.android.features.invite.test.InMemorySeenInvitesStore
1515
import io.element.android.features.preferences.impl.DefaultCacheService
1616
import io.element.android.libraries.matrix.api.core.SessionId
1717
import io.element.android.libraries.matrix.test.A_ROOM_ID
18+
import io.element.android.libraries.matrix.test.A_SESSION_ID
1819
import io.element.android.libraries.matrix.test.FakeMatrixClient
20+
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
1921
import io.element.android.libraries.push.test.FakePushService
22+
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
2023
import io.element.android.tests.testutils.lambda.lambdaRecorder
2124
import io.element.android.tests.testutils.lambda.value
2225
import io.element.android.tests.testutils.testCoroutineDispatchers
@@ -31,8 +34,10 @@ import org.robolectric.RobolectricTestRunner
3134
class DefaultClearCacheUseCaseTest {
3235
@Test
3336
fun `execute clear cache should do all the expected tasks`() = runTest {
37+
val activeRoomsHolder = ActiveRoomsHolder().apply { addRoom(FakeJoinedRoom()) }
3438
val clearCacheLambda = lambdaRecorder<Unit> { }
3539
val matrixClient = FakeMatrixClient(
40+
sessionId = A_SESSION_ID,
3641
clearCacheLambda = clearCacheLambda,
3742
)
3843
val defaultCacheService = DefaultCacheService()
@@ -55,6 +60,7 @@ class DefaultClearCacheUseCaseTest {
5560
ftueService = ftueService,
5661
pushService = pushService,
5762
seenInvitesStore = seenInvitesStore,
63+
activeRoomsHolder = activeRoomsHolder,
5864
)
5965
defaultCacheService.clearedCacheEventFlow.test {
6066
sut.invoke()
@@ -64,6 +70,7 @@ class DefaultClearCacheUseCaseTest {
6470
.with(value(matrixClient.sessionId), value(false))
6571
assertThat(awaitItem()).isEqualTo(matrixClient.sessionId)
6672
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
73+
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
6774
}
6875
}
6976
}

features/share/impl/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ dependencies {
3737
implementation(projects.libraries.roomselect.api)
3838
implementation(projects.libraries.uiStrings)
3939
implementation(projects.libraries.testtags)
40+
implementation(projects.services.appnavstate.api)
4041
api(libs.statemachine)
4142
api(projects.features.share.api)
4243

0 commit comments

Comments
 (0)