Add ActiveRoomsHolder to manage the active rooms for a session (#4758)
This commit is contained in:
parent
b39100ef6f
commit
c67089edf7
19 changed files with 240 additions and 30 deletions
|
|
@ -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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
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 io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -51,6 +52,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
roomComponentFactory: RoomComponentFactory,
|
roomComponentFactory: RoomComponentFactory,
|
||||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
|
|
@ -85,6 +87,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
onCreate = {
|
onCreate = {
|
||||||
Timber.v("OnCreate => ${inputs.room.roomId}")
|
Timber.v("OnCreate => ${inputs.room.roomId}")
|
||||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||||
|
activeRoomsHolder.addRoom(inputs.room)
|
||||||
fetchRoomMembers()
|
fetchRoomMembers()
|
||||||
trackVisitedRoom()
|
trackVisitedRoom()
|
||||||
},
|
},
|
||||||
|
|
@ -95,6 +98,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
},
|
},
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
Timber.v("OnDestroy")
|
Timber.v("OnDestroy")
|
||||||
|
activeRoomsHolder.removeRoom(inputs.room.sessionId, inputs.room.roomId)
|
||||||
inputs.room.destroy()
|
inputs.room.destroy()
|
||||||
appNavigationStateService.onLeavingRoom(id)
|
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.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
import io.element.android.libraries.architecture.childNode
|
import io.element.android.libraries.architecture.childNode
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
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.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
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.room.FakeJoinedRoom
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class JoinBaseRoomLoadedFlowNodeTest {
|
class JoinedRoomLoadedFlowNodeTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
|
@ -100,6 +102,7 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||||
plugins: List<Plugin>,
|
plugins: List<Plugin>,
|
||||||
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
||||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||||
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
) = JoinedRoomLoadedFlowNode(
|
) = JoinedRoomLoadedFlowNode(
|
||||||
buildContext = BuildContext.root(savedStateMap = null),
|
buildContext = BuildContext.root(savedStateMap = null),
|
||||||
|
|
@ -110,6 +113,7 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||||
appCoroutineScope = coroutineScope,
|
appCoroutineScope = coroutineScope,
|
||||||
roomComponentFactory = FakeRoomComponentFactory(),
|
roomComponentFactory = FakeRoomComponentFactory(),
|
||||||
matrixClient = FakeMatrixClient(),
|
matrixClient = FakeMatrixClient(),
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -154,4 +158,55 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||||
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
||||||
assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
|
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.matrix.api.widget.MatrixWidgetDriver
|
||||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||||
import io.element.android.services.analytics.api.ScreenTracker
|
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.appnavstate.api.AppForegroundStateService
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -62,6 +63,7 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
private val activeCallManager: ActiveCallManager,
|
private val activeCallManager: ActiveCallManager,
|
||||||
private val languageTagProvider: LanguageTagProvider,
|
private val languageTagProvider: LanguageTagProvider,
|
||||||
private val appForegroundStateService: AppForegroundStateService,
|
private val appForegroundStateService: AppForegroundStateService,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
) : Presenter<CallScreenState> {
|
) : Presenter<CallScreenState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -241,8 +243,10 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
|
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
|
||||||
if (!notifiedCallStart) {
|
if (!notifiedCallStart) {
|
||||||
getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
|
val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
|
||||||
?.onSuccess { notifiedCallStart = true }
|
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.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ class DefaultCallWidgetProvider @Inject constructor(
|
||||||
private val matrixClientsProvider: MatrixClientProvider,
|
private val matrixClientsProvider: MatrixClientProvider,
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
) : CallWidgetProvider {
|
) : CallWidgetProvider {
|
||||||
override suspend fun getWidget(
|
override suspend fun getWidget(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
|
|
@ -33,7 +35,9 @@ class DefaultCallWidgetProvider @Inject constructor(
|
||||||
theme: String?,
|
theme: String?,
|
||||||
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
|
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
|
||||||
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
|
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 customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
|
||||||
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
|
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.libraries.network.useragent.UserAgentProvider
|
||||||
import io.element.android.services.analytics.api.ScreenTracker
|
import io.element.android.services.analytics.api.ScreenTracker
|
||||||
import io.element.android.services.analytics.test.FakeScreenTracker
|
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.appnavstate.test.FakeAppForegroundStateService
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
|
@ -367,6 +368,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
|
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
|
||||||
screenTracker: ScreenTracker = FakeScreenTracker(),
|
screenTracker: ScreenTracker = FakeScreenTracker(),
|
||||||
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
||||||
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
): CallScreenPresenter {
|
): CallScreenPresenter {
|
||||||
val userAgentProvider = object : UserAgentProvider {
|
val userAgentProvider = object : UserAgentProvider {
|
||||||
override fun provide(): String {
|
override fun provide(): String {
|
||||||
|
|
@ -387,6 +389,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
languageTagProvider = FakeLanguageTagProvider("en-US"),
|
languageTagProvider = FakeLanguageTagProvider("en-US"),
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
appCoroutineScope = backgroundScope,
|
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.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
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.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -77,6 +79,23 @@ class DefaultCallWidgetProviderTest {
|
||||||
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull()
|
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
|
@Test
|
||||||
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
|
|
@ -104,9 +123,11 @@ class DefaultCallWidgetProviderTest {
|
||||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
||||||
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
) = DefaultCallWidgetProvider(
|
) = DefaultCallWidgetProvider(
|
||||||
matrixClientsProvider = matrixClientProvider,
|
matrixClientsProvider = matrixClientProvider,
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
callWidgetSettingsProvider = callWidgetSettingsProvider,
|
callWidgetSettingsProvider = callWidgetSettingsProvider,
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ dependencies {
|
||||||
implementation(projects.features.roomlist.api)
|
implementation(projects.features.roomlist.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.services.analytics.compose)
|
implementation(projects.services.analytics.compose)
|
||||||
|
implementation(projects.services.appnavstate.api)
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
implementation(libs.coil.compose)
|
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.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -37,8 +38,11 @@ class DefaultClearCacheUseCase @Inject constructor(
|
||||||
private val ftueService: FtueService,
|
private val ftueService: FtueService,
|
||||||
private val pushService: PushService,
|
private val pushService: PushService,
|
||||||
private val seenInvitesStore: SeenInvitesStore,
|
private val seenInvitesStore: SeenInvitesStore,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
) : ClearCacheUseCase {
|
) : ClearCacheUseCase {
|
||||||
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
|
||||||
|
// Active rooms should be disposed of before clearing the cache
|
||||||
|
activeRoomsHolder.clear(matrixClient.sessionId)
|
||||||
// Clear Matrix cache
|
// Clear Matrix cache
|
||||||
matrixClient.clearCache()
|
matrixClient.clearCache()
|
||||||
// Clear Coil cache
|
// 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.features.preferences.impl.DefaultCacheService
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
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_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.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.push.test.FakePushService
|
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.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
|
@ -31,8 +34,10 @@ import org.robolectric.RobolectricTestRunner
|
||||||
class DefaultClearCacheUseCaseTest {
|
class DefaultClearCacheUseCaseTest {
|
||||||
@Test
|
@Test
|
||||||
fun `execute clear cache should do all the expected tasks`() = runTest {
|
fun `execute clear cache should do all the expected tasks`() = runTest {
|
||||||
|
val activeRoomsHolder = ActiveRoomsHolder().apply { addRoom(FakeJoinedRoom()) }
|
||||||
val clearCacheLambda = lambdaRecorder<Unit> { }
|
val clearCacheLambda = lambdaRecorder<Unit> { }
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
clearCacheLambda = clearCacheLambda,
|
clearCacheLambda = clearCacheLambda,
|
||||||
)
|
)
|
||||||
val defaultCacheService = DefaultCacheService()
|
val defaultCacheService = DefaultCacheService()
|
||||||
|
|
@ -55,6 +60,7 @@ class DefaultClearCacheUseCaseTest {
|
||||||
ftueService = ftueService,
|
ftueService = ftueService,
|
||||||
pushService = pushService,
|
pushService = pushService,
|
||||||
seenInvitesStore = seenInvitesStore,
|
seenInvitesStore = seenInvitesStore,
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
defaultCacheService.clearedCacheEventFlow.test {
|
defaultCacheService.clearedCacheEventFlow.test {
|
||||||
sut.invoke()
|
sut.invoke()
|
||||||
|
|
@ -64,6 +70,7 @@ class DefaultClearCacheUseCaseTest {
|
||||||
.with(value(matrixClient.sessionId), value(false))
|
.with(value(matrixClient.sessionId), value(false))
|
||||||
assertThat(awaitItem()).isEqualTo(matrixClient.sessionId)
|
assertThat(awaitItem()).isEqualTo(matrixClient.sessionId)
|
||||||
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
|
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.roomselect.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
|
implementation(projects.services.appnavstate.api)
|
||||||
api(libs.statemachine)
|
api(libs.statemachine)
|
||||||
api(projects.features.share.api)
|
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.core.bool.orFalse
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
@ -33,6 +35,7 @@ class SharePresenter @AssistedInject constructor(
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val mediaPreProcessor: MediaPreProcessor,
|
private val mediaPreProcessor: MediaPreProcessor,
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
) : Presenter<ShareState> {
|
) : Presenter<ShareState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
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(
|
private fun CoroutineScope.share(
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
roomIds: List<RoomId>,
|
roomIds: List<RoomId>,
|
||||||
|
|
@ -72,7 +81,7 @@ class SharePresenter @AssistedInject constructor(
|
||||||
} else {
|
} else {
|
||||||
roomIds
|
roomIds
|
||||||
.map { roomId ->
|
.map { roomId ->
|
||||||
val room = matrixClient.getJoinedRoom(roomId) ?: return@map false
|
val room = getJoinedRoom(roomId) ?: return@map false
|
||||||
val mediaSender = MediaSender(
|
val mediaSender = MediaSender(
|
||||||
preProcessor = mediaPreProcessor,
|
preProcessor = mediaPreProcessor,
|
||||||
room = room,
|
room = room,
|
||||||
|
|
@ -86,7 +95,11 @@ class SharePresenter @AssistedInject constructor(
|
||||||
).isSuccess
|
).isSuccess
|
||||||
}
|
}
|
||||||
.all { it }
|
.all { it }
|
||||||
.also { room.destroy() }
|
.also {
|
||||||
|
if (activeRoomsHolder.getActiveRoomMatching(matrixClient.sessionId, roomId) == null) {
|
||||||
|
room.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.all { it }
|
.all { it }
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +107,7 @@ class SharePresenter @AssistedInject constructor(
|
||||||
onPlainText = { text ->
|
onPlainText = { text ->
|
||||||
roomIds
|
roomIds
|
||||||
.map { roomId ->
|
.map { roomId ->
|
||||||
matrixClient.getJoinedRoom(roomId)?.liveTimeline?.sendMessage(
|
getJoinedRoom(roomId)?.liveTimeline?.sendMessage(
|
||||||
body = text,
|
body = text,
|
||||||
htmlBody = null,
|
htmlBody = null,
|
||||||
intentionalMentions = emptyList(),
|
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.api.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
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.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -163,7 +164,8 @@ class SharePresenterTest {
|
||||||
intent: Intent = Intent(),
|
intent: Intent = Intent(),
|
||||||
shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(),
|
shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(),
|
||||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor()
|
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||||
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
): SharePresenter {
|
): SharePresenter {
|
||||||
return SharePresenter(
|
return SharePresenter(
|
||||||
intent = intent,
|
intent = intent,
|
||||||
|
|
@ -171,7 +173,8 @@ class SharePresenterTest {
|
||||||
shareIntentHandler = shareIntentHandler,
|
shareIntentHandler = shareIntentHandler,
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
mediaPreProcessor = mediaPreProcessor,
|
mediaPreProcessor = mediaPreProcessor,
|
||||||
InMemorySessionPreferencesStore(),
|
sessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -416,7 +416,6 @@ class JoinedRustRoom(
|
||||||
RustWidgetDriver(
|
RustWidgetDriver(
|
||||||
widgetSettings = widgetSettings,
|
widgetSettings = widgetSettings,
|
||||||
room = innerRoom,
|
room = innerRoom,
|
||||||
joinedRustRoom = this,
|
|
||||||
widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider {
|
widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider {
|
||||||
override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities {
|
override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities {
|
||||||
return getElementCallRequiredPermissions(sessionId.value, baseRoom.deviceId.value)
|
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.MatrixWidgetDriver
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -25,7 +24,6 @@ import kotlin.coroutines.coroutineContext
|
||||||
class RustWidgetDriver(
|
class RustWidgetDriver(
|
||||||
widgetSettings: MatrixWidgetSettings,
|
widgetSettings: MatrixWidgetSettings,
|
||||||
private val room: Room,
|
private val room: Room,
|
||||||
private val joinedRustRoom: JoinedRustRoom,
|
|
||||||
private val widgetCapabilitiesProvider: WidgetCapabilitiesProvider,
|
private val widgetCapabilitiesProvider: WidgetCapabilitiesProvider,
|
||||||
) : MatrixWidgetDriver {
|
) : MatrixWidgetDriver {
|
||||||
// It's important to have extra capacity here to make sure we don't drop any messages
|
// 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() {
|
override fun close() {
|
||||||
receiveMessageJob?.cancel()
|
receiveMessageJob?.cancel()
|
||||||
driverAndHandle.driver.close()
|
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.R
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||||
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
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.strings.StringProvider
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -44,6 +45,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
||||||
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val replyMessageExtractor: ReplyMessageExtractor,
|
private val replyMessageExtractor: ReplyMessageExtractor,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
) {
|
) {
|
||||||
fun onReceive(intent: Intent) {
|
fun onReceive(intent: Intent) {
|
||||||
val sessionId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return
|
val sessionId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return
|
||||||
|
|
@ -117,13 +119,15 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: 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(
|
sendMatrixEvent(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
replyToEventId = replyToEventId,
|
replyToEventId = replyToEventId,
|
||||||
threadId = threadId,
|
threadId = threadId,
|
||||||
room = room,
|
room = it,
|
||||||
message = message,
|
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.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
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 io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -30,28 +31,43 @@ class SyncOnNotifiableEvent @Inject constructor(
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val appForegroundStateService: AppForegroundStateService,
|
private val appForegroundStateService: AppForegroundStateService,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(notifiableEvent: NotifiableEvent) = withContext(dispatchers.io) {
|
suspend operator fun invoke(notifiableEvent: NotifiableEvent) = withContext(dispatchers.io) {
|
||||||
val isRingingCallEvent = notifiableEvent is NotifiableRingingCallEvent
|
val isRingingCallEvent = notifiableEvent is NotifiableRingingCallEvent
|
||||||
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush) && !isRingingCallEvent) {
|
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush) && !isRingingCallEvent) {
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
val client = matrixClientProvider.getOrRestore(notifiableEvent.sessionId).getOrNull() ?: return@withContext
|
|
||||||
|
|
||||||
client.getJoinedRoom(notifiableEvent.roomId)?.use { room ->
|
val activeRoom = activeRoomsHolder.getActiveRoomMatching(notifiableEvent.sessionId, notifiableEvent.roomId)
|
||||||
room.subscribeToSync()
|
|
||||||
|
|
||||||
// If the app is in foreground, sync is already running, so we just add the subscription above.
|
if (activeRoom != null) {
|
||||||
if (!appForegroundStateService.isInForeground.value) {
|
// If the room is already active, we can use it directly
|
||||||
if (isRingingCallEvent) {
|
activeRoom.subscribeToSyncAndWait(notifiableEvent, isRingingCallEvent)
|
||||||
room.waitsUntilUserIsInTheCall(timeout = 60.seconds)
|
} else {
|
||||||
} else {
|
// Otherwise, we need to get the room from the matrix client
|
||||||
try {
|
val room = matrixClientProvider
|
||||||
appForegroundStateService.updateIsSyncingNotificationEvent(true)
|
.getOrRestore(notifiableEvent.sessionId)
|
||||||
room.waitsUntilEventIsKnown(eventId = notifiableEvent.eventId, timeout = 10.seconds)
|
.mapCatching { it.getJoinedRoom(notifiableEvent.roomId) }
|
||||||
} finally {
|
.getOrNull()
|
||||||
appForegroundStateService.updateIsSyncingNotificationEvent(false)
|
|
||||||
}
|
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.FakeOnNotifiableEventReceived
|
||||||
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
||||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
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.strings.StringProvider
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||||
|
|
@ -477,6 +478,7 @@ class NotificationBroadcastReceiverHandlerTest {
|
||||||
onNotifiableEventReceived: OnNotifiableEventReceived = FakeOnNotifiableEventReceived(),
|
onNotifiableEventReceived: OnNotifiableEventReceived = FakeOnNotifiableEventReceived(),
|
||||||
stringProvider: StringProvider = FakeStringProvider(),
|
stringProvider: StringProvider = FakeStringProvider(),
|
||||||
replyMessageExtractor: ReplyMessageExtractor = FakeReplyMessageExtractor(),
|
replyMessageExtractor: ReplyMessageExtractor = FakeReplyMessageExtractor(),
|
||||||
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
): NotificationBroadcastReceiverHandler {
|
): NotificationBroadcastReceiverHandler {
|
||||||
return NotificationBroadcastReceiverHandler(
|
return NotificationBroadcastReceiverHandler(
|
||||||
appCoroutineScope = this,
|
appCoroutineScope = this,
|
||||||
|
|
@ -494,6 +496,7 @@ class NotificationBroadcastReceiverHandlerTest {
|
||||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||||
stringProvider = stringProvider,
|
stringProvider = stringProvider,
|
||||||
replyMessageExtractor = replyMessageExtractor,
|
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.matrix.test.timeline.anEventTimelineItem
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
|
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
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.services.appnavstate.test.FakeAppForegroundStateService
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -199,7 +200,8 @@ class SyncOnNotifiableEventTest {
|
||||||
isSyncOnPushEnabled: Boolean = true,
|
isSyncOnPushEnabled: Boolean = true,
|
||||||
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(
|
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(
|
||||||
initialForegroundValue = true,
|
initialForegroundValue = true,
|
||||||
)
|
),
|
||||||
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
): SyncOnNotifiableEvent {
|
): SyncOnNotifiableEvent {
|
||||||
val featureFlagService = FakeFeatureFlagService(
|
val featureFlagService = FakeFeatureFlagService(
|
||||||
initialState = mapOf(
|
initialState = mapOf(
|
||||||
|
|
@ -212,6 +214,7 @@ class SyncOnNotifiableEventTest {
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
dispatchers = testCoroutineDispatchers(),
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue