Add ActiveRoomsHolder to manage the active rooms for a session (#4758)
This commit is contained in:
committed by
GitHub
parent
b39100ef6f
commit
c67089edf7
@@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -51,6 +52,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
roomComponentFactory: RoomComponentFactory,
|
||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
@@ -85,6 +87,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||
onCreate = {
|
||||
Timber.v("OnCreate => ${inputs.room.roomId}")
|
||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||
activeRoomsHolder.addRoom(inputs.room)
|
||||
fetchRoomMembers()
|
||||
trackVisitedRoom()
|
||||
},
|
||||
@@ -95,6 +98,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||
},
|
||||
onDestroy = {
|
||||
Timber.v("OnDestroy")
|
||||
activeRoomsHolder.removeRoom(inputs.room.sessionId, inputs.room.roomId)
|
||||
inputs.room.destroy()
|
||||
appNavigationStateService.onLeavingRoom(id)
|
||||
}
|
||||
|
||||
@@ -24,16 +24,18 @@ import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.libraries.architecture.childNode
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class JoinBaseRoomLoadedFlowNodeTest {
|
||||
class JoinedRoomLoadedFlowNodeTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@@ -100,6 +102,7 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||
plugins: List<Plugin>,
|
||||
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
coroutineScope: CoroutineScope,
|
||||
) = JoinedRoomLoadedFlowNode(
|
||||
buildContext = BuildContext.root(savedStateMap = null),
|
||||
@@ -110,6 +113,7 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||
appCoroutineScope = coroutineScope,
|
||||
roomComponentFactory = FakeRoomComponentFactory(),
|
||||
matrixClient = FakeMatrixClient(),
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
|
||||
@Test
|
||||
@@ -154,4 +158,55 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
||||
assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the ActiveRoomsHolder will be updated with the loaded room on create`() = runTest {
|
||||
// GIVEN
|
||||
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||
val activeRoomsHolder = ActiveRoomsHolder()
|
||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
plugins = listOf(inputs),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||
coroutineScope = this,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
|
||||
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
|
||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||
// WHEN
|
||||
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.CREATED)
|
||||
// THEN
|
||||
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the ActiveRoomsHolder will be removed on destroy`() = runTest {
|
||||
// GIVEN
|
||||
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||
val activeRoomsHolder = ActiveRoomsHolder().apply {
|
||||
addRoom(room)
|
||||
}
|
||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
plugins = listOf(inputs),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||
coroutineScope = this,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.CREATED)
|
||||
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNotNull()
|
||||
// WHEN
|
||||
roomFlowNode.updateLifecycleState(Lifecycle.State.DESTROYED)
|
||||
// THEN
|
||||
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.DESTROYED)
|
||||
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import io.element.android.services.analytics.api.ScreenTracker
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -62,6 +63,7 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||
private val activeCallManager: ActiveCallManager,
|
||||
private val languageTagProvider: LanguageTagProvider,
|
||||
private val appForegroundStateService: AppForegroundStateService,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
) : Presenter<CallScreenState> {
|
||||
@AssistedFactory
|
||||
@@ -241,8 +243,10 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||
|
||||
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
|
||||
if (!notifiedCallStart) {
|
||||
getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
|
||||
?.onSuccess { notifiedCallStart = true }
|
||||
val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
|
||||
val sendCallNotificationResult = activeRoomForSession?.sendCallNotificationIfNeeded()
|
||||
?: getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
|
||||
sendCallNotificationResult?.onSuccess { notifiedCallStart = true }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -24,6 +25,7 @@ class DefaultCallWidgetProvider @Inject constructor(
|
||||
private val matrixClientsProvider: MatrixClientProvider,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
) : CallWidgetProvider {
|
||||
override suspend fun getWidget(
|
||||
sessionId: SessionId,
|
||||
@@ -33,7 +35,9 @@ class DefaultCallWidgetProvider @Inject constructor(
|
||||
theme: String?,
|
||||
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
|
||||
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
|
||||
val room = matrixClient.getJoinedRoom(roomId) ?: error("Room not found")
|
||||
val room = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
|
||||
?: matrixClient.getJoinedRoom(roomId)
|
||||
?: error("Room not found")
|
||||
|
||||
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
|
||||
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
|
||||
|
||||
@@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import io.element.android.services.analytics.api.ScreenTracker
|
||||
import io.element.android.services.analytics.test.FakeScreenTracker
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
@@ -367,6 +368,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
|
||||
screenTracker: ScreenTracker = FakeScreenTracker(),
|
||||
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
): CallScreenPresenter {
|
||||
val userAgentProvider = object : UserAgentProvider {
|
||||
override fun provide(): String {
|
||||
@@ -387,6 +389,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
languageTagProvider = FakeLanguageTagProvider("en-US"),
|
||||
appForegroundStateService = appForegroundStateService,
|
||||
appCoroutineScope = backgroundScope,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,13 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
|
||||
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@@ -77,6 +79,23 @@ class DefaultCallWidgetProviderTest {
|
||||
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getWidget - reuses the active room if possible`() = runTest {
|
||||
val client = FakeMatrixClient().apply {
|
||||
// No room from the client
|
||||
givenGetRoomResult(A_ROOM_ID, null)
|
||||
}
|
||||
val activeRoomsHolder = ActiveRoomsHolder().apply {
|
||||
// A current active room with the same room id
|
||||
addRoom(FakeJoinedRoom(baseRoom = FakeBaseRoom(roomId = A_ROOM_ID)))
|
||||
}
|
||||
val provider = createProvider(
|
||||
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
|
||||
activeRoomsHolder = activeRoomsHolder
|
||||
)
|
||||
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
||||
val room = FakeJoinedRoom(
|
||||
@@ -104,9 +123,11 @@ class DefaultCallWidgetProviderTest {
|
||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
) = DefaultCallWidgetProvider(
|
||||
matrixClientsProvider = matrixClientProvider,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
callWidgetSettingsProvider = callWidgetSettingsProvider,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ dependencies {
|
||||
implementation(projects.features.roomlist.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.services.analytics.compose)
|
||||
implementation(projects.services.appnavstate.api)
|
||||
implementation(projects.services.toolbox.api)
|
||||
implementation(libs.datetime)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
@@ -18,6 +18,7 @@ import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Inject
|
||||
@@ -37,8 +38,11 @@ class DefaultClearCacheUseCase @Inject constructor(
|
||||
private val ftueService: FtueService,
|
||||
private val pushService: PushService,
|
||||
private val seenInvitesStore: SeenInvitesStore,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
) : ClearCacheUseCase {
|
||||
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
||||
// Active rooms should be disposed of before clearing the cache
|
||||
activeRoomsHolder.clear(matrixClient.sessionId)
|
||||
// Clear Matrix cache
|
||||
matrixClient.clearCache()
|
||||
// Clear Coil cache
|
||||
|
||||
@@ -15,8 +15,11 @@ import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||
import io.element.android.features.preferences.impl.DefaultCacheService
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.push.test.FakePushService
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
@@ -31,8 +34,10 @@ import org.robolectric.RobolectricTestRunner
|
||||
class DefaultClearCacheUseCaseTest {
|
||||
@Test
|
||||
fun `execute clear cache should do all the expected tasks`() = runTest {
|
||||
val activeRoomsHolder = ActiveRoomsHolder().apply { addRoom(FakeJoinedRoom()) }
|
||||
val clearCacheLambda = lambdaRecorder<Unit> { }
|
||||
val matrixClient = FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
clearCacheLambda = clearCacheLambda,
|
||||
)
|
||||
val defaultCacheService = DefaultCacheService()
|
||||
@@ -55,6 +60,7 @@ class DefaultClearCacheUseCaseTest {
|
||||
ftueService = ftueService,
|
||||
pushService = pushService,
|
||||
seenInvitesStore = seenInvitesStore,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
defaultCacheService.clearedCacheEventFlow.test {
|
||||
sut.invoke()
|
||||
@@ -64,6 +70,7 @@ class DefaultClearCacheUseCaseTest {
|
||||
.with(value(matrixClient.sessionId), value(false))
|
||||
assertThat(awaitItem()).isEqualTo(matrixClient.sessionId)
|
||||
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
|
||||
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ dependencies {
|
||||
implementation(projects.libraries.roomselect.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.services.appnavstate.api)
|
||||
api(libs.statemachine)
|
||||
api(projects.features.share.api)
|
||||
|
||||
|
||||
@@ -20,9 +20,11 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -33,6 +35,7 @@ class SharePresenter @AssistedInject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val mediaPreProcessor: MediaPreProcessor,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
) : Presenter<ShareState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
@@ -59,6 +62,12 @@ class SharePresenter @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? {
|
||||
return activeRoomsHolder.getActiveRoom(matrixClient.sessionId)
|
||||
?.takeIf { it.roomId == roomId }
|
||||
?: matrixClient.getJoinedRoom(roomId)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.share(
|
||||
intent: Intent,
|
||||
roomIds: List<RoomId>,
|
||||
@@ -72,7 +81,7 @@ class SharePresenter @AssistedInject constructor(
|
||||
} else {
|
||||
roomIds
|
||||
.map { roomId ->
|
||||
val room = matrixClient.getJoinedRoom(roomId) ?: return@map false
|
||||
val room = getJoinedRoom(roomId) ?: return@map false
|
||||
val mediaSender = MediaSender(
|
||||
preProcessor = mediaPreProcessor,
|
||||
room = room,
|
||||
@@ -86,7 +95,11 @@ class SharePresenter @AssistedInject constructor(
|
||||
).isSuccess
|
||||
}
|
||||
.all { it }
|
||||
.also { room.destroy() }
|
||||
.also {
|
||||
if (activeRoomsHolder.getActiveRoomMatching(matrixClient.sessionId, roomId) == null) {
|
||||
room.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
.all { it }
|
||||
}
|
||||
@@ -94,7 +107,7 @@ class SharePresenter @AssistedInject constructor(
|
||||
onPlainText = { text ->
|
||||
roomIds
|
||||
.map { roomId ->
|
||||
matrixClient.getJoinedRoom(roomId)?.liveTimeline?.sendMessage(
|
||||
getJoinedRoom(roomId)?.liveTimeline?.sendMessage(
|
||||
body = text,
|
||||
htmlBody = null,
|
||||
intentionalMentions = emptyList(),
|
||||
|
||||
@@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
@@ -163,7 +164,8 @@ class SharePresenterTest {
|
||||
intent: Intent = Intent(),
|
||||
shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(),
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor()
|
||||
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
): SharePresenter {
|
||||
return SharePresenter(
|
||||
intent = intent,
|
||||
@@ -171,7 +173,8 @@ class SharePresenterTest {
|
||||
shareIntentHandler = shareIntentHandler,
|
||||
matrixClient = matrixClient,
|
||||
mediaPreProcessor = mediaPreProcessor,
|
||||
InMemorySessionPreferencesStore(),
|
||||
sessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,7 +416,6 @@ class JoinedRustRoom(
|
||||
RustWidgetDriver(
|
||||
widgetSettings = widgetSettings,
|
||||
room = innerRoom,
|
||||
joinedRustRoom = this,
|
||||
widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider {
|
||||
override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities {
|
||||
return getElementCallRequiredPermissions(sessionId.value, baseRoom.deviceId.value)
|
||||
|
||||
@@ -9,7 +9,6 @@ package io.element.android.libraries.matrix.impl.widget
|
||||
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
import io.element.android.libraries.matrix.impl.room.JoinedRustRoom
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -25,7 +24,6 @@ import kotlin.coroutines.coroutineContext
|
||||
class RustWidgetDriver(
|
||||
widgetSettings: MatrixWidgetSettings,
|
||||
private val room: Room,
|
||||
private val joinedRustRoom: JoinedRustRoom,
|
||||
private val widgetCapabilitiesProvider: WidgetCapabilitiesProvider,
|
||||
) : MatrixWidgetDriver {
|
||||
// It's important to have extra capacity here to make sure we don't drop any messages
|
||||
@@ -71,6 +69,5 @@ class RustWidgetDriver(
|
||||
override fun close() {
|
||||
receiveMessageJob?.cancel()
|
||||
driverAndHandle.driver.close()
|
||||
joinedRustRoom.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -44,6 +45,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
||||
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
||||
private val stringProvider: StringProvider,
|
||||
private val replyMessageExtractor: ReplyMessageExtractor,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
) {
|
||||
fun onReceive(intent: Intent) {
|
||||
val sessionId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return
|
||||
@@ -117,13 +119,15 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
||||
return@launch
|
||||
}
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||
client.getJoinedRoom(roomId)?.let { room ->
|
||||
val room = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId) ?: client.getJoinedRoom(roomId)
|
||||
|
||||
room?.let {
|
||||
sendMatrixEvent(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
replyToEventId = replyToEventId,
|
||||
threadId = threadId,
|
||||
room = room,
|
||||
room = it,
|
||||
message = message,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -30,28 +31,43 @@ class SyncOnNotifiableEvent @Inject constructor(
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val appForegroundStateService: AppForegroundStateService,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
) {
|
||||
suspend operator fun invoke(notifiableEvent: NotifiableEvent) = withContext(dispatchers.io) {
|
||||
val isRingingCallEvent = notifiableEvent is NotifiableRingingCallEvent
|
||||
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush) && !isRingingCallEvent) {
|
||||
return@withContext
|
||||
}
|
||||
val client = matrixClientProvider.getOrRestore(notifiableEvent.sessionId).getOrNull() ?: return@withContext
|
||||
|
||||
client.getJoinedRoom(notifiableEvent.roomId)?.use { room ->
|
||||
room.subscribeToSync()
|
||||
val activeRoom = activeRoomsHolder.getActiveRoomMatching(notifiableEvent.sessionId, notifiableEvent.roomId)
|
||||
|
||||
// If the app is in foreground, sync is already running, so we just add the subscription above.
|
||||
if (!appForegroundStateService.isInForeground.value) {
|
||||
if (isRingingCallEvent) {
|
||||
room.waitsUntilUserIsInTheCall(timeout = 60.seconds)
|
||||
} else {
|
||||
try {
|
||||
appForegroundStateService.updateIsSyncingNotificationEvent(true)
|
||||
room.waitsUntilEventIsKnown(eventId = notifiableEvent.eventId, timeout = 10.seconds)
|
||||
} finally {
|
||||
appForegroundStateService.updateIsSyncingNotificationEvent(false)
|
||||
}
|
||||
if (activeRoom != null) {
|
||||
// If the room is already active, we can use it directly
|
||||
activeRoom.subscribeToSyncAndWait(notifiableEvent, isRingingCallEvent)
|
||||
} else {
|
||||
// Otherwise, we need to get the room from the matrix client
|
||||
val room = matrixClientProvider
|
||||
.getOrRestore(notifiableEvent.sessionId)
|
||||
.mapCatching { it.getJoinedRoom(notifiableEvent.roomId) }
|
||||
.getOrNull()
|
||||
|
||||
room?.use { it.subscribeToSyncAndWait(notifiableEvent, isRingingCallEvent) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun JoinedRoom.subscribeToSyncAndWait(notifiableEvent: NotifiableEvent, isRingingCallEvent: Boolean) {
|
||||
subscribeToSync()
|
||||
|
||||
// If the app is in foreground, sync is already running, so we just add the subscription above.
|
||||
if (!appForegroundStateService.isInForeground.value) {
|
||||
if (isRingingCallEvent) {
|
||||
waitsUntilUserIsInTheCall(timeout = 60.seconds)
|
||||
} else {
|
||||
try {
|
||||
appForegroundStateService.updateIsSyncingNotificationEvent(true)
|
||||
waitsUntilEventIsKnown(eventId = notifiableEvent.eventId, timeout = 10.seconds)
|
||||
} finally {
|
||||
appForegroundStateService.updateIsSyncingNotificationEvent(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven
|
||||
import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived
|
||||
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
@@ -477,6 +478,7 @@ class NotificationBroadcastReceiverHandlerTest {
|
||||
onNotifiableEventReceived: OnNotifiableEventReceived = FakeOnNotifiableEventReceived(),
|
||||
stringProvider: StringProvider = FakeStringProvider(),
|
||||
replyMessageExtractor: ReplyMessageExtractor = FakeReplyMessageExtractor(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
): NotificationBroadcastReceiverHandler {
|
||||
return NotificationBroadcastReceiverHandler(
|
||||
appCoroutineScope = this,
|
||||
@@ -494,6 +496,7 @@ class NotificationBroadcastReceiverHandlerTest {
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
stringProvider = stringProvider,
|
||||
replyMessageExtractor = replyMessageExtractor,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
@@ -199,7 +200,8 @@ class SyncOnNotifiableEventTest {
|
||||
isSyncOnPushEnabled: Boolean = true,
|
||||
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(
|
||||
initialForegroundValue = true,
|
||||
)
|
||||
),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
): SyncOnNotifiableEvent {
|
||||
val featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(
|
||||
@@ -212,6 +214,7 @@ class SyncOnNotifiableEventTest {
|
||||
featureFlagService = featureFlagService,
|
||||
appForegroundStateService = appForegroundStateService,
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.services.appnavstate.api
|
||||
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Holds the active rooms for a given session so they can be reused instead of instantiating new ones.
|
||||
*/
|
||||
@SingleIn(AppScope::class)
|
||||
class ActiveRoomsHolder @Inject constructor() {
|
||||
private val rooms = ConcurrentHashMap<SessionId, MutableSet<JoinedRoom>>()
|
||||
|
||||
/**
|
||||
* Adds a new held room for the given sessionId.
|
||||
*/
|
||||
fun addRoom(room: JoinedRoom) {
|
||||
val roomsForSessionId = rooms.getOrPut(key = room.sessionId, defaultValue = { mutableSetOf() })
|
||||
if (roomsForSessionId.none { it.roomId == room.roomId }) {
|
||||
// We don't want to add the same room multiple times
|
||||
roomsForSessionId.add(room)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last room added for the given [sessionId] or null if no room was added.
|
||||
*/
|
||||
fun getActiveRoom(sessionId: SessionId): JoinedRoom? {
|
||||
return rooms[sessionId]?.lastOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an active room associated to the given [sessionId], with the given [roomId], or null if none match.
|
||||
*/
|
||||
fun getActiveRoomMatching(sessionId: SessionId, roomId: RoomId): JoinedRoom? {
|
||||
return rooms[sessionId]?.find { it.roomId == roomId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any room matching the provided [sessionId] and [roomId].
|
||||
*/
|
||||
fun removeRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
val roomsForSessionId = rooms[sessionId] ?: return
|
||||
roomsForSessionId.removeIf { it.roomId == roomId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all the rooms for the given sessionId.
|
||||
*/
|
||||
fun clear(sessionId: SessionId) {
|
||||
val activeRooms = rooms.remove(sessionId) ?: return
|
||||
for (room in activeRooms) {
|
||||
// Destroy the room to reset the live timelines
|
||||
room.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user