Remember flows (#4533)
* Add Konsist test to ensure that the result of a function returning a flow is remembered. * Remember flows before they are collected by state. * Fix compilation issue * Make isOnline a val. * Make selectedUsers() a val. * Make flow() a val. * Make getUserConsent(), didAskUserConsent() and getAnalyticsId() some val. * Remove Timeline.paginationStatus() and replace by direct access to the underlined flow. * Simplify test * userConsentFlow must be initialized before because it's used in observeUserConsent * Fix test compilation
This commit is contained in:
parent
3e90b3c26d
commit
77a7c0b2e5
52 changed files with 221 additions and 172 deletions
|
|
@ -30,7 +30,6 @@ import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
|
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.isOnline
|
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
|
@ -79,7 +78,7 @@ class LoggedInPresenter @Inject constructor(
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
}
|
}
|
||||||
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
|
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
|
||||||
val isOnline by syncService.isOnline().collectAsState()
|
val isOnline by syncService.isOnline.collectAsState()
|
||||||
val showSyncSpinner by remember {
|
val showSyncSpinner by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
isOnline && syncIndicator == RoomListService.SyncIndicator.Show
|
isOnline && syncIndicator == RoomListService.SyncIndicator.Show
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.isOnline
|
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -211,7 +210,7 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadingNode(buildContext: BuildContext) = node(buildContext) { modifier ->
|
private fun loadingNode(buildContext: BuildContext) = node(buildContext) { modifier ->
|
||||||
val isOnline by syncService.isOnline().collectAsState()
|
val isOnline by syncService.isOnline.collectAsState()
|
||||||
LoadingRoomNodeView(
|
LoadingRoomNodeView(
|
||||||
state = LoadingRoomState.Loading,
|
state = LoadingRoomState.Loading,
|
||||||
hasNetworkConnection = isOnline,
|
hasNetworkConnection = isOnline,
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
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.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.isOnline
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
@ -114,7 +113,7 @@ class JoinedRoomFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
private fun loadingNode(buildContext: BuildContext, onBackClick: () -> Unit) = node(buildContext) { modifier ->
|
private fun loadingNode(buildContext: BuildContext, onBackClick: () -> Unit) = node(buildContext) { modifier ->
|
||||||
val loadingRoomState by loadingRoomStateStateFlow.collectAsState()
|
val loadingRoomState by loadingRoomStateStateFlow.collectAsState()
|
||||||
val isOnline by syncService.isOnline().collectAsState()
|
val isOnline by syncService.isOnline.collectAsState()
|
||||||
LoadingRoomNodeView(
|
LoadingRoomNodeView(
|
||||||
state = loadingRoomState,
|
state = loadingRoomState,
|
||||||
hasNetworkConnection = isOnline,
|
hasNetworkConnection = isOnline,
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@ class AnalyticsPreferencesPresenter @Inject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): AnalyticsPreferencesState {
|
override fun present(): AnalyticsPreferencesState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val isEnabled = analyticsService.getUserConsent()
|
val isEnabled = analyticsService.userConsentFlow.collectAsState(initial = false)
|
||||||
.collectAsState(initial = false)
|
|
||||||
|
|
||||||
fun handleEvents(event: AnalyticsOptInEvents) {
|
fun handleEvents(event: AnalyticsOptInEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,10 @@ class AnalyticsOptInPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(analyticsService.didAskUserConsent().first()).isFalse()
|
assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse()
|
||||||
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true))
|
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true))
|
||||||
assertThat(analyticsService.didAskUserConsent().first()).isTrue()
|
assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue()
|
||||||
assertThat(analyticsService.getUserConsent().first()).isTrue()
|
assertThat(analyticsService.userConsentFlow.first()).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,10 +53,10 @@ class AnalyticsOptInPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(analyticsService.didAskUserConsent().first()).isFalse()
|
assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse()
|
||||||
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false))
|
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false))
|
||||||
assertThat(analyticsService.didAskUserConsent().first()).isTrue()
|
assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue()
|
||||||
assertThat(analyticsService.getUserConsent().first()).isFalse()
|
assertThat(analyticsService.userConsentFlow.first()).isFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class CreateRoomDataStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val createRoomConfigWithInvites: Flow<CreateRoomConfig> = combine(
|
val createRoomConfigWithInvites: Flow<CreateRoomConfig> = combine(
|
||||||
selectedUserListDataStore.selectedUsers(),
|
selectedUserListDataStore.selectedUsers,
|
||||||
createRoomConfigFlow,
|
createRoomConfigFlow,
|
||||||
) { selectedUsers, config ->
|
) { selectedUsers, config ->
|
||||||
config.copy(invites = selectedUsers.toImmutableList())
|
config.copy(invites = selectedUsers.toImmutableList())
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,9 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
val cameraPermissionState = cameraPermissionPresenter.present()
|
val cameraPermissionState = cameraPermissionPresenter.present()
|
||||||
val createRoomConfig by dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig())
|
val createRoomConfig by dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig())
|
||||||
val homeserverName = remember { matrixClient.userIdServerName() }
|
val homeserverName = remember { matrixClient.userIdServerName() }
|
||||||
val isKnockFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(initial = false)
|
val isKnockFeatureEnabled by remember {
|
||||||
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
|
||||||
|
}.collectAsState(initial = false)
|
||||||
val roomAddressValidity = remember {
|
val roomAddressValidity = remember {
|
||||||
mutableStateOf<RoomAddressValidity>(RoomAddressValidity.Unknown)
|
mutableStateOf<RoomAddressValidity>(RoomAddressValidity.Unknown)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,9 @@ class CreateRoomRootPresenter @Inject constructor(
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
val isRoomDirectorySearchEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch).collectAsState(initial = false)
|
val isRoomDirectorySearchEnabled by remember {
|
||||||
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch)
|
||||||
|
}.collectAsState(initial = false)
|
||||||
|
|
||||||
fun handleEvents(event: CreateRoomRootEvents) {
|
fun handleEvents(event: CreateRoomRootEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
recentDirectRooms = matrixClient.getRecentDirectRooms()
|
recentDirectRooms = matrixClient.getRecentDirectRooms()
|
||||||
}
|
}
|
||||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
|
val selectedUsers by userListDataStore.selectedUsers.collectAsState(emptyList())
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
var searchResults: SearchBarResultState<ImmutableList<UserSearchResult>> by remember {
|
var searchResults: SearchBarResultState<ImmutableList<UserSearchResult>> by remember {
|
||||||
mutableStateOf(SearchBarResultState.Initial())
|
mutableStateOf(SearchBarResultState.Initial())
|
||||||
|
|
|
||||||
|
|
@ -8,22 +8,22 @@
|
||||||
package io.element.android.features.createroom.impl.userlist
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class UserListDataStore @Inject constructor() {
|
class UserListDataStore @Inject constructor() {
|
||||||
private val selectedUsers: MutableStateFlow<List<MatrixUser>> = MutableStateFlow(emptyList())
|
private val _selectedUsers: MutableStateFlow<List<MatrixUser>> = MutableStateFlow(emptyList())
|
||||||
|
|
||||||
fun selectUser(user: MatrixUser) {
|
fun selectUser(user: MatrixUser) {
|
||||||
if (!selectedUsers.value.contains(user)) {
|
if (!_selectedUsers.value.contains(user)) {
|
||||||
selectedUsers.tryEmit(selectedUsers.value.plus(user))
|
_selectedUsers.tryEmit(_selectedUsers.value.plus(user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeUserFromSelection(user: MatrixUser) {
|
fun removeUserFromSelection(user: MatrixUser) {
|
||||||
selectedUsers.tryEmit(selectedUsers.value.minus(user))
|
_selectedUsers.tryEmit(_selectedUsers.value.minus(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectedUsers(): Flow<List<MatrixUser>> = selectedUsers
|
val selectedUsers = _selectedUsers.asStateFlow()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
moveToNextStepIfNeeded()
|
moveToNextStepIfNeeded()
|
||||||
})
|
})
|
||||||
|
|
||||||
analyticsService.didAskUserConsent()
|
analyticsService.didAskUserConsentFlow
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach { moveToNextStepIfNeeded() }
|
.onEach { moveToNextStepIfNeeded() }
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ class DefaultFtueService @Inject constructor(
|
||||||
.onEach { updateState() }
|
.onEach { updateState() }
|
||||||
.launchIn(sessionCoroutineScope)
|
.launchIn(sessionCoroutineScope)
|
||||||
|
|
||||||
analyticsService.didAskUserConsent()
|
analyticsService.didAskUserConsentFlow
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach { updateState() }
|
.onEach { updateState() }
|
||||||
.launchIn(sessionCoroutineScope)
|
.launchIn(sessionCoroutineScope)
|
||||||
|
|
@ -118,7 +118,7 @@ class DefaultFtueService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun needsAnalyticsOptIn(): Boolean {
|
private suspend fun needsAnalyticsOptIn(): Boolean {
|
||||||
return analyticsService.didAskUserConsent().first().not()
|
return analyticsService.didAskUserConsentFlow.first().not()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun shouldAskNotificationPermissions(): Boolean {
|
private suspend fun shouldAskNotificationPermissions(): Boolean {
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,9 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
override fun present(): JoinRoomState {
|
override fun present(): JoinRoomState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var retryCount by remember { mutableIntStateOf(0) }
|
var retryCount by remember { mutableIntStateOf(0) }
|
||||||
val roomInfo by matrixClient.getRoomInfoFlow(roomId.toRoomIdOrAlias()).collectAsState(initial = Optional.empty())
|
val roomInfo by remember {
|
||||||
|
matrixClient.getRoomInfoFlow(roomId.toRoomIdOrAlias())
|
||||||
|
}.collectAsState(initial = Optional.empty())
|
||||||
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
val cancelKnockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val cancelKnockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,9 @@ class DefaultBiometricAuthenticatorManager @Inject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun rememberUnlockBiometricAuthenticator(): BiometricAuthenticator {
|
override fun rememberUnlockBiometricAuthenticator(): BiometricAuthenticator {
|
||||||
val isBiometricAllowed by lockScreenStore.isBiometricUnlockAllowed().collectAsState(initial = false)
|
val isBiometricAllowed by remember {
|
||||||
|
lockScreenStore.isBiometricUnlockAllowed()
|
||||||
|
}.collectAsState(initial = false)
|
||||||
val lifecycleState by LocalLifecycleOwner.current.lifecycle.currentStateFlow.collectAsState()
|
val lifecycleState by LocalLifecycleOwner.current.lifecycle.currentStateFlow.collectAsState()
|
||||||
val isAvailable by remember(lifecycleState) {
|
val isAvailable by remember(lifecycleState) {
|
||||||
derivedStateOf { isBiometricAllowed && hasAvailableAuthenticator }
|
derivedStateOf { isBiometricAllowed && hasAvailableAuthenticator }
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@ class LockScreenSettingsPresenter @Inject constructor(
|
||||||
value = !lockScreenConfig.isPinMandatory && hasPinCode
|
value = !lockScreenConfig.isPinMandatory && hasPinCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val isBiometricEnabled by lockScreenStore.isBiometricUnlockAllowed().collectAsState(initial = false)
|
val isBiometricEnabled by remember {
|
||||||
|
lockScreenStore.isBiometricUnlockAllowed()
|
||||||
|
}.collectAsState(initial = false)
|
||||||
var showRemovePinConfirmation by remember {
|
var showRemovePinConfirmation by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,7 @@ class AccountProviderDataSource @Inject constructor(
|
||||||
defaultAccountProvider
|
defaultAccountProvider
|
||||||
)
|
)
|
||||||
|
|
||||||
fun flow(): StateFlow<AccountProvider> {
|
val flow: StateFlow<AccountProvider> = accountProvider.asStateFlow()
|
||||||
return accountProvider.asStateFlow()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
accountProvider.tryEmit(defaultAccountProvider)
|
accountProvider.tryEmit(defaultAccountProvider)
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ConfirmAccountProviderState {
|
override fun present(): ConfirmAccountProviderState {
|
||||||
val accountProvider by accountProviderDataSource.flow().collectAsState()
|
val accountProvider by accountProviderDataSource.flow.collectAsState()
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val loginFlowAction: MutableState<AsyncData<LoginFlow>> = remember {
|
val loginFlowAction: MutableState<AsyncData<LoginFlow>> = remember {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class DefaultMessageParser @Inject constructor(
|
||||||
val parser = Json { ignoreUnknownKeys = true }
|
val parser = Json { ignoreUnknownKeys = true }
|
||||||
val response = parser.decodeFromString(MobileRegistrationResponse.serializer(), message)
|
val response = parser.decodeFromString(MobileRegistrationResponse.serializer(), message)
|
||||||
val userId = response.userId ?: error("No user ID in response")
|
val userId = response.userId ?: error("No user ID in response")
|
||||||
val homeServer = response.homeServer ?: accountProviderDataSource.flow().value.url
|
val homeServer = response.homeServer ?: accountProviderDataSource.flow.value.url
|
||||||
val accessToken = response.accessToken ?: error("No access token in response")
|
val accessToken = response.accessToken ?: error("No access token in response")
|
||||||
val deviceId = response.deviceId ?: error("No device ID in response")
|
val deviceId = response.deviceId ?: error("No device ID in response")
|
||||||
return ExternalSession(
|
return ExternalSession(
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class LoginPasswordPresenter @Inject constructor(
|
||||||
val formState = rememberSaveable {
|
val formState = rememberSaveable {
|
||||||
mutableStateOf(LoginFormState.Default)
|
mutableStateOf(LoginFormState.Default)
|
||||||
}
|
}
|
||||||
val accountProvider by accountProviderDataSource.flow().collectAsState()
|
val accountProvider by accountProviderDataSource.flow.collectAsState()
|
||||||
|
|
||||||
fun handleEvents(event: LoginPasswordEvents) {
|
fun handleEvents(event: LoginPasswordEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class AccountProviderDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val sut = AccountProviderDataSource(FakeEnterpriseService())
|
val sut = AccountProviderDataSource(FakeEnterpriseService())
|
||||||
sut.flow().test {
|
sut.flow.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState).isEqualTo(
|
assertThat(initialState).isEqualTo(
|
||||||
AccountProvider(
|
AccountProvider(
|
||||||
|
|
@ -43,7 +43,7 @@ class AccountProviderDataSourceTest {
|
||||||
val sut = AccountProviderDataSource(FakeEnterpriseService(
|
val sut = AccountProviderDataSource(FakeEnterpriseService(
|
||||||
defaultHomeserverResult = { AuthenticationConfig.MATRIX_ORG_URL }
|
defaultHomeserverResult = { AuthenticationConfig.MATRIX_ORG_URL }
|
||||||
))
|
))
|
||||||
sut.flow().test {
|
sut.flow.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState).isEqualTo(
|
assertThat(initialState).isEqualTo(
|
||||||
AccountProvider(
|
AccountProvider(
|
||||||
|
|
@ -61,7 +61,7 @@ class AccountProviderDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - user change and reset`() = runTest {
|
fun `present - user change and reset`() = runTest {
|
||||||
val sut = AccountProviderDataSource(FakeEnterpriseService())
|
val sut = AccountProviderDataSource(FakeEnterpriseService())
|
||||||
sut.flow().test {
|
sut.flow.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER)
|
assertThat(initialState.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER)
|
||||||
sut.userSelection(AccountProvider(url = "https://example.com"))
|
sut.userSelection(AccountProvider(url = "https://example.com"))
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,6 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
|
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.isOnline
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
|
@ -183,7 +182,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
showReinvitePrompt = !hasDismissedInviteDialog && composerHasFocus && roomInfo.isDm && roomInfo.activeMembersCount == 1L
|
showReinvitePrompt = !hasDismissedInviteDialog && composerHasFocus && roomInfo.isDm && roomInfo.activeMembersCount == 1L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val isOnline by syncService.isOnline().collectAsState()
|
val isOnline by syncService.isOnline.collectAsState()
|
||||||
|
|
||||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,9 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
mutableStateOf(ActionListState.Target.None)
|
mutableStateOf(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isDeveloperModeEnabled by appPreferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
|
val isDeveloperModeEnabled by remember {
|
||||||
|
appPreferencesStore.isDeveloperModeEnabledFlow()
|
||||||
|
}.collectAsState(initial = false)
|
||||||
val isPinnedEventsEnabled = isPinnedMessagesFeatureEnabled()
|
val isPinnedEventsEnabled = isPinnedMessagesFeatureEnabled()
|
||||||
val pinnedEventIds by remember {
|
val pinnedEventIds by remember {
|
||||||
room.roomInfoFlow.map { it.pinnedEventIds }
|
room.roomInfoFlow.map { it.pinnedEventIds }
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,12 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
||||||
|
|
||||||
val allowCaption by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionCreation).collectAsState(initial = false)
|
val allowCaption by remember {
|
||||||
val showCaptionCompatibilityWarning by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionWarning).collectAsState(initial = false)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionCreation)
|
||||||
|
}.collectAsState(initial = false)
|
||||||
|
val showCaptionCompatibilityWarning by remember {
|
||||||
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionWarning)
|
||||||
|
}.collectAsState(initial = false)
|
||||||
|
|
||||||
var useSendQueue by remember { mutableStateOf(false) }
|
var useSendQueue by remember { mutableStateOf(false) }
|
||||||
var preprocessMediaJob by remember { mutableStateOf<Job?>(null) }
|
var preprocessMediaJob by remember { mutableStateOf<Job?>(null) }
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,9 @@ class MessageComposerPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
|
var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val sendTypingNotifications by sessionPreferencesStore.isSendTypingNotificationsEnabled().collectAsState(initial = true)
|
val sendTypingNotifications by remember {
|
||||||
|
sessionPreferencesStore.isSendTypingNotificationsEnabled()
|
||||||
|
}.collectAsState(initial = true)
|
||||||
|
|
||||||
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
||||||
if (cameraPermissionState.permissionGranted) {
|
if (cameraPermissionState.permissionGranted) {
|
||||||
|
|
@ -397,16 +399,16 @@ class MessageComposerPresenter @AssistedInject constructor(
|
||||||
.stateIn(this, SharingStarted.Lazily, emptyList())
|
.stateIn(this, SharingStarted.Lazily, emptyList())
|
||||||
|
|
||||||
combine(mentionTriggerFlow, room.membersStateFlow, roomAliasSuggestionsFlow) { suggestion, roomMembersState, roomAliasSuggestions ->
|
combine(mentionTriggerFlow, room.membersStateFlow, roomAliasSuggestionsFlow) { suggestion, roomMembersState, roomAliasSuggestions ->
|
||||||
val result = suggestionsProcessor.process(
|
val result = suggestionsProcessor.process(
|
||||||
suggestion = suggestion,
|
suggestion = suggestion,
|
||||||
roomMembersState = roomMembersState,
|
roomMembersState = roomMembersState,
|
||||||
roomAliasSuggestions = roomAliasSuggestions,
|
roomAliasSuggestions = roomAliasSuggestions,
|
||||||
currentUserId = currentUserId,
|
currentUserId = currentUserId,
|
||||||
canSendRoomMention = ::canSendRoomMention,
|
canSendRoomMention = ::canSendRoomMention,
|
||||||
)
|
)
|
||||||
suggestions.clear()
|
suggestions.clear()
|
||||||
suggestions.addAll(result)
|
suggestions.addAll(result)
|
||||||
}
|
}
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,9 +109,15 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
val messageShield: MutableState<MessageShield?> = remember { mutableStateOf(null) }
|
val messageShield: MutableState<MessageShield?> = remember { mutableStateOf(null) }
|
||||||
|
|
||||||
val resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailurePresenter.present()
|
val resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailurePresenter.present()
|
||||||
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
val isSendPublicReadReceiptsEnabled by remember {
|
||||||
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
|
sessionPreferencesStore.isSendPublicReadReceiptsEnabled()
|
||||||
val isLive by timelineController.isLive().collectAsState(initial = true)
|
}.collectAsState(initial = true)
|
||||||
|
val renderReadReceipts by remember {
|
||||||
|
sessionPreferencesStore.isRenderReadReceiptsEnabled()
|
||||||
|
}.collectAsState(initial = true)
|
||||||
|
val isLive by remember {
|
||||||
|
timelineController.isLive()
|
||||||
|
}.collectAsState(initial = true)
|
||||||
|
|
||||||
fun handleEvents(event: TimelineEvents) {
|
fun handleEvents(event: TimelineEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ class TimelineProtectionPresenter @Inject constructor(
|
||||||
) : Presenter<TimelineProtectionState> {
|
) : Presenter<TimelineProtectionState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): TimelineProtectionState {
|
override fun present(): TimelineProtectionState {
|
||||||
val hideMediaContent by appPreferencesStore.doesHideImagesAndVideosFlow().collectAsState(initial = false)
|
val hideMediaContent by remember {
|
||||||
|
appPreferencesStore.doesHideImagesAndVideosFlow()
|
||||||
|
}.collectAsState(initial = false)
|
||||||
var allowedEvents by remember { mutableStateOf<Set<EventId>>(setOf()) }
|
var allowedEvents by remember { mutableStateOf<Set<EventId>>(setOf()) }
|
||||||
val protectionState by remember(hideMediaContent) {
|
val protectionState by remember(hideMediaContent) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@ class TypingNotificationPresenter @Inject constructor(
|
||||||
) : Presenter<TypingNotificationState> {
|
) : Presenter<TypingNotificationState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): TypingNotificationState {
|
override fun present(): TypingNotificationState {
|
||||||
val renderTypingNotifications by sessionPreferencesStore.isRenderTypingNotificationsEnabled().collectAsState(initial = true)
|
val renderTypingNotifications by remember {
|
||||||
|
sessionPreferencesStore.isRenderTypingNotificationsEnabled()
|
||||||
|
}.collectAsState(initial = true)
|
||||||
val typingMembersState by produceState(initialValue = persistentListOf(), key1 = renderTypingNotifications) {
|
val typingMembersState by produceState(initialValue = persistentListOf(), key1 = renderTypingNotifications) {
|
||||||
if (renderTypingNotifications) {
|
if (renderTypingNotifications) {
|
||||||
observeRoomTypingMembers()
|
observeRoomTypingMembers()
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ class MigrationPresenter @Inject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): MigrationState {
|
override fun present(): MigrationState {
|
||||||
val migrationStoreVersion by migrationStore.applicationMigrationVersion().collectAsState(initial = null)
|
val migrationStoreVersion by remember {
|
||||||
|
migrationStore.applicationMigrationVersion()
|
||||||
|
}.collectAsState(initial = null)
|
||||||
var migrationAction: AsyncData<Unit> by remember { mutableStateOf(AsyncData.Uninitialized) }
|
var migrationAction: AsyncData<Unit> by remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||||
|
|
||||||
// Uncomment this block to run the migration everytime
|
// Uncomment this block to run the migration everytime
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class PollHistoryPresenter @Inject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PollHistoryState {
|
override fun present(): PollHistoryState {
|
||||||
val timeline = room.liveTimeline
|
val timeline = room.liveTimeline
|
||||||
val paginationState by timeline.paginationStatus(Timeline.PaginationDirection.BACKWARDS).collectAsState()
|
val paginationState by timeline.backwardPaginationStatus.collectAsState()
|
||||||
val pollHistoryItemsFlow = remember {
|
val pollHistoryItemsFlow = remember {
|
||||||
timeline.timelineItems.map { items ->
|
timeline.timelineItems.map { items ->
|
||||||
pollHistoryItemFactory.create(items)
|
pollHistoryItemFactory.create(items)
|
||||||
|
|
|
||||||
|
|
@ -29,19 +29,18 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): AdvancedSettingsState {
|
override fun present(): AdvancedSettingsState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val isDeveloperModeEnabled by appPreferencesStore
|
val isDeveloperModeEnabled by remember {
|
||||||
.isDeveloperModeEnabledFlow()
|
appPreferencesStore.isDeveloperModeEnabledFlow()
|
||||||
.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
val isSharePresenceEnabled by sessionPreferencesStore
|
val isSharePresenceEnabled by remember {
|
||||||
.isSharePresenceEnabled()
|
sessionPreferencesStore.isSharePresenceEnabled()
|
||||||
.collectAsState(initial = true)
|
}.collectAsState(initial = true)
|
||||||
val doesCompressMedia by sessionPreferencesStore
|
val doesCompressMedia by remember {
|
||||||
.doesCompressMedia()
|
sessionPreferencesStore.doesCompressMedia()
|
||||||
.collectAsState(initial = true)
|
}.collectAsState(initial = true)
|
||||||
val theme by remember {
|
val theme by remember {
|
||||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||||
}
|
}.collectAsState(initial = Theme.System)
|
||||||
.collectAsState(initial = Theme.System)
|
|
||||||
var showChangeThemeDialog by remember { mutableStateOf(false) }
|
var showChangeThemeDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
fun handleEvents(event: AdvancedSettingsEvents) {
|
fun handleEvents(event: AdvancedSettingsEvents) {
|
||||||
|
|
|
||||||
|
|
@ -44,17 +44,17 @@ class BlockedUsersPresenter @Inject constructor(
|
||||||
mutableStateOf(AsyncAction.Uninitialized)
|
mutableStateOf(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
val renderBlockedUsersDetail = featureFlagService
|
val renderBlockedUsersDetail by remember {
|
||||||
.isFeatureEnabledFlow(FeatureFlags.ShowBlockedUsersDetails)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.ShowBlockedUsersDetails)
|
||||||
.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
val ignoredUserIds by matrixClient.ignoredUsersFlow.collectAsState()
|
val ignoredUserIds by matrixClient.ignoredUsersFlow.collectAsState()
|
||||||
val ignoredMatrixUser by produceState(
|
val ignoredMatrixUser by produceState(
|
||||||
initialValue = ignoredUserIds.map { MatrixUser(userId = it) },
|
initialValue = ignoredUserIds.map { MatrixUser(userId = it) },
|
||||||
key1 = renderBlockedUsersDetail.value,
|
key1 = renderBlockedUsersDetail,
|
||||||
key2 = ignoredUserIds
|
key2 = ignoredUserIds
|
||||||
) {
|
) {
|
||||||
value = ignoredUserIds.map {
|
value = ignoredUserIds.map {
|
||||||
if (renderBlockedUsersDetail.value) {
|
if (renderBlockedUsersDetail) {
|
||||||
matrixClient.getProfile(it).getOrNull()
|
matrixClient.getProfile(it).getOrNull()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|
|
||||||
|
|
@ -71,12 +71,14 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
val clearCacheAction = remember {
|
val clearCacheAction = remember {
|
||||||
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
val customElementCallBaseUrl by appPreferencesStore
|
val customElementCallBaseUrl by remember {
|
||||||
.getCustomElementCallBaseUrlFlow()
|
appPreferencesStore
|
||||||
.collectAsState(initial = null)
|
.getCustomElementCallBaseUrlFlow()
|
||||||
val hideImagesAndVideos by appPreferencesStore
|
}.collectAsState(initial = null)
|
||||||
.doesHideImagesAndVideosFlow()
|
val hideImagesAndVideos by remember {
|
||||||
.collectAsState(initial = false)
|
appPreferencesStore
|
||||||
|
.doesHideImagesAndVideosFlow()
|
||||||
|
}.collectAsState(initial = false)
|
||||||
|
|
||||||
val tracingLogLevelFlow = remember {
|
val tracingLogLevelFlow = remember {
|
||||||
appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) }
|
appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) }
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,9 @@ class NotificationSettingsPresenter @Inject constructor(
|
||||||
val changeNotificationSettingAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
val changeNotificationSettingAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val appNotificationsEnabled = userPushStore
|
val appNotificationsEnabled by remember {
|
||||||
.getNotificationEnabledForDevice()
|
userPushStore.getNotificationEnabledForDevice()
|
||||||
.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
|
|
||||||
val matrixSettings: MutableState<NotificationSettingsState.MatrixSettings> = remember {
|
val matrixSettings: MutableState<NotificationSettingsState.MatrixSettings> = remember {
|
||||||
mutableStateOf(NotificationSettingsState.MatrixSettings.Uninitialized)
|
mutableStateOf(NotificationSettingsState.MatrixSettings.Uninitialized)
|
||||||
|
|
@ -158,7 +158,7 @@ class NotificationSettingsPresenter @Inject constructor(
|
||||||
matrixSettings = matrixSettings.value,
|
matrixSettings = matrixSettings.value,
|
||||||
appSettings = NotificationSettingsState.AppSettings(
|
appSettings = NotificationSettingsState.AppSettings(
|
||||||
systemNotificationsEnabled = systemNotificationsEnabled.value,
|
systemNotificationsEnabled = systemNotificationsEnabled.value,
|
||||||
appNotificationsEnabled = appNotificationsEnabled.value
|
appNotificationsEnabled = appNotificationsEnabled,
|
||||||
),
|
),
|
||||||
changeNotificationSettingAction = changeNotificationSettingAction.value,
|
changeNotificationSettingAction = changeNotificationSettingAction.value,
|
||||||
currentPushDistributor = currentDistributor,
|
currentPushDistributor = currentDistributor,
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,9 @@ class BugReportPresenter @Inject constructor(
|
||||||
screenshotHolder.getFileUri()
|
screenshotHolder.getFileUri()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val crashInfo: String by crashDataStore
|
val crashInfo: String by remember {
|
||||||
.crashInfo()
|
crashDataStore.crashInfo()
|
||||||
.collectAsState(initial = "")
|
}.collectAsState(initial = "")
|
||||||
|
|
||||||
val sendingProgress = remember {
|
val sendingProgress = remember {
|
||||||
mutableFloatStateOf(0f)
|
mutableFloatStateOf(0f)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.features.rageshake.impl.preferences
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
|
@ -39,13 +40,13 @@ class DefaultRageshakePreferencesPresenter @Inject constructor(
|
||||||
mutableStateOf(rageshake.isAvailable())
|
mutableStateOf(rageshake.isAvailable())
|
||||||
}
|
}
|
||||||
val isFeatureAvailable = remember { rageshakeFeatureAvailability.isAvailable() }
|
val isFeatureAvailable = remember { rageshakeFeatureAvailability.isAvailable() }
|
||||||
val isEnabled = rageshakeDataStore
|
val isEnabled by remember {
|
||||||
.isEnabled()
|
rageshakeDataStore.isEnabled()
|
||||||
.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
|
|
||||||
val sensitivity = rageshakeDataStore
|
val sensitivity by remember {
|
||||||
.sensitivity()
|
rageshakeDataStore.sensitivity()
|
||||||
.collectAsState(initial = 0f)
|
}.collectAsState(initial = 0f)
|
||||||
|
|
||||||
fun handleEvents(event: RageshakePreferencesEvents) {
|
fun handleEvents(event: RageshakePreferencesEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
@ -56,9 +57,9 @@ class DefaultRageshakePreferencesPresenter @Inject constructor(
|
||||||
|
|
||||||
return RageshakePreferencesState(
|
return RageshakePreferencesState(
|
||||||
isFeatureEnabled = isFeatureAvailable,
|
isFeatureEnabled = isFeatureAvailable,
|
||||||
isEnabled = isEnabled.value,
|
isEnabled = isEnabled,
|
||||||
isSupported = isSupported.value,
|
isSupported = isSupported.value,
|
||||||
sensitivity = sensitivity.value,
|
sensitivity = sensitivity,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,9 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val canHandleKnockRequests by room.canHandleKnockRequestsAsState(syncUpdateFlow.value)
|
val canHandleKnockRequests by room.canHandleKnockRequestsAsState(syncUpdateFlow.value)
|
||||||
val isKnockRequestsEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(false)
|
val isKnockRequestsEnabled by remember {
|
||||||
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
|
||||||
|
}.collectAsState(false)
|
||||||
val knockRequestsCount by produceState<Int?>(null) {
|
val knockRequestsCount by produceState<Int?>(null) {
|
||||||
room.knockRequestsFlow.collect { value = it.size }
|
room.knockRequestsFlow.collect { value = it.size }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.isOnline
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
|
|
@ -101,7 +100,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val leaveRoomState = leaveRoomPresenter.present()
|
val leaveRoomState = leaveRoomPresenter.present()
|
||||||
val matrixUser = client.userProfile.collectAsState()
|
val matrixUser = client.userProfile.collectAsState()
|
||||||
val isOnline by syncService.isOnline().collectAsState()
|
val isOnline by syncService.isOnline.collectAsState()
|
||||||
val filtersState = filtersPresenter.present()
|
val filtersState = filtersPresenter.present()
|
||||||
val searchState = searchPresenter.present()
|
val searchState = searchPresenter.present()
|
||||||
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,9 @@ class SignedOutPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): SignedOutState {
|
override fun present(): SignedOutState {
|
||||||
val sessions by sessionStore.sessionsFlow().collectAsState(initial = emptyList())
|
val sessions by remember {
|
||||||
|
sessionStore.sessionsFlow()
|
||||||
|
}.collectAsState(initial = emptyList())
|
||||||
val signedOutSession by remember {
|
val signedOutSession by remember {
|
||||||
derivedStateOf { sessions.firstOrNull { it.userId == sessionId } }
|
derivedStateOf { sessions.firstOrNull { it.userId == sessionId } }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.sync
|
package io.element.android.libraries.matrix.api.sync
|
||||||
|
|
||||||
import io.element.android.libraries.core.coroutine.mapState
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface SyncService {
|
interface SyncService {
|
||||||
|
|
@ -25,6 +24,6 @@ interface SyncService {
|
||||||
* Flow of [SyncState]. Will be updated as soon as the current [SyncState] changes.
|
* Flow of [SyncState]. Will be updated as soon as the current [SyncState] changes.
|
||||||
*/
|
*/
|
||||||
val syncState: StateFlow<SyncState>
|
val syncState: StateFlow<SyncState>
|
||||||
}
|
|
||||||
|
|
||||||
fun SyncService.isOnline(): StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
|
val isOnline: StateFlow<Boolean>
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,10 @@ interface Timeline : AutoCloseable {
|
||||||
val membershipChangeEventReceived: Flow<Unit>
|
val membershipChangeEventReceived: Flow<Unit>
|
||||||
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
|
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
|
||||||
suspend fun paginate(direction: PaginationDirection): Result<Boolean>
|
suspend fun paginate(direction: PaginationDirection): Result<Boolean>
|
||||||
fun paginationStatus(direction: PaginationDirection): StateFlow<PaginationStatus>
|
|
||||||
|
val backwardPaginationStatus: StateFlow<PaginationStatus>
|
||||||
|
val forwardPaginationStatus: StateFlow<PaginationStatus>
|
||||||
|
|
||||||
val timelineItems: Flow<List<MatrixTimelineItem>>
|
val timelineItems: Flow<List<MatrixTimelineItem>>
|
||||||
|
|
||||||
suspend fun sendMessage(
|
suspend fun sendMessage(
|
||||||
|
|
@ -105,7 +108,7 @@ interface Timeline : AutoCloseable {
|
||||||
caption: String?,
|
caption: String?,
|
||||||
formattedCaption: String?,
|
formattedCaption: String?,
|
||||||
progressCallback: ProgressCallback?,
|
progressCallback: ProgressCallback?,
|
||||||
): Result<MediaUploadHandler>
|
): Result<MediaUploadHandler>
|
||||||
|
|
||||||
suspend fun sendFile(
|
suspend fun sendFile(
|
||||||
file: File,
|
file: File,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.sync
|
package io.element.android.libraries.matrix.impl.sync
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.coroutine.mapState
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
|
@ -73,4 +74,6 @@ class RustSyncService(
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle)
|
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle)
|
||||||
|
|
||||||
|
override val isOnline: StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -127,11 +126,11 @@ class RustTimeline(
|
||||||
private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode)
|
private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode)
|
||||||
private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode)
|
private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode)
|
||||||
|
|
||||||
private val backPaginationStatus = MutableStateFlow(
|
override val backwardPaginationStatus = MutableStateFlow(
|
||||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PINNED_EVENTS)
|
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PINNED_EVENTS)
|
||||||
)
|
)
|
||||||
|
|
||||||
private val forwardPaginationStatus = MutableStateFlow(
|
override val forwardPaginationStatus = MutableStateFlow(
|
||||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode == Timeline.Mode.FOCUSED_ON_EVENT)
|
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode == Timeline.Mode.FOCUSED_ON_EVENT)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -167,7 +166,7 @@ class RustTimeline(
|
||||||
|
|
||||||
private fun updatePaginationStatus(direction: Timeline.PaginationDirection, update: (Timeline.PaginationStatus) -> Timeline.PaginationStatus) {
|
private fun updatePaginationStatus(direction: Timeline.PaginationDirection, update: (Timeline.PaginationStatus) -> Timeline.PaginationStatus) {
|
||||||
when (direction) {
|
when (direction) {
|
||||||
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.getAndUpdate(update)
|
Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus.getAndUpdate(update)
|
||||||
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.getAndUpdate(update)
|
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.getAndUpdate(update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +184,7 @@ class RustTimeline(
|
||||||
}
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
if (error is TimelineException.CannotPaginate) {
|
if (error is TimelineException.CannotPaginate) {
|
||||||
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}")
|
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backwardPaginationStatus.value}")
|
||||||
} else {
|
} else {
|
||||||
updatePaginationStatus(direction) { it.copy(isPaginating = false) }
|
updatePaginationStatus(direction) { it.copy(isPaginating = false) }
|
||||||
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
|
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
|
||||||
|
|
@ -199,21 +198,14 @@ class RustTimeline(
|
||||||
private fun canPaginate(direction: Timeline.PaginationDirection): Boolean {
|
private fun canPaginate(direction: Timeline.PaginationDirection): Boolean {
|
||||||
if (!isTimelineInitialized.value) return false
|
if (!isTimelineInitialized.value) return false
|
||||||
return when (direction) {
|
return when (direction) {
|
||||||
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.value.canPaginate
|
Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus.value.canPaginate
|
||||||
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.value.canPaginate
|
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.value.canPaginate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow<Timeline.PaginationStatus> {
|
|
||||||
return when (direction) {
|
|
||||||
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus
|
|
||||||
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
|
override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
|
||||||
_timelineItems,
|
_timelineItems,
|
||||||
backPaginationStatus,
|
backwardPaginationStatus,
|
||||||
forwardPaginationStatus,
|
forwardPaginationStatus,
|
||||||
matrixRoom.roomInfoFlow.map { it.creator to it.isDm }.distinctUntilChanged(),
|
matrixRoom.roomInfoFlow.map { it.creator to it.isDm }.distinctUntilChanged(),
|
||||||
isTimelineInitialized,
|
isTimelineInitialized,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class DefaultCallWidgetSettingsProvider @Inject constructor(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
) : CallWidgetSettingsProvider {
|
) : CallWidgetSettingsProvider {
|
||||||
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings {
|
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings {
|
||||||
val isAnalyticsEnabled = analyticsService.getUserConsent().first()
|
val isAnalyticsEnabled = analyticsService.userConsentFlow.first()
|
||||||
val options = VirtualElementCallWidgetOptions(
|
val options = VirtualElementCallWidgetOptions(
|
||||||
elementCallUrl = baseUrl,
|
elementCallUrl = baseUrl,
|
||||||
widgetId = widgetId,
|
widgetId = widgetId,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.test.sync
|
package io.element.android.libraries.matrix.test.sync
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.coroutine.mapState
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -29,6 +30,8 @@ class FakeSyncService(
|
||||||
|
|
||||||
override val syncState: StateFlow<SyncState> = syncStateFlow
|
override val syncState: StateFlow<SyncState> = syncStateFlow
|
||||||
|
|
||||||
|
override val isOnline: StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
|
||||||
|
|
||||||
suspend fun emitSyncState(syncState: SyncState) {
|
suspend fun emitSyncState(syncState: SyncState) {
|
||||||
syncStateFlow.emit(syncState)
|
syncStateFlow.emit(syncState)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,19 +28,18 @@ import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class FakeTimeline(
|
class FakeTimeline(
|
||||||
private val name: String = "FakeTimeline",
|
private val name: String = "FakeTimeline",
|
||||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = MutableStateFlow(emptyList()),
|
override val timelineItems: Flow<List<MatrixTimelineItem>> = MutableStateFlow(emptyList()),
|
||||||
private val backwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
|
override val backwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
|
||||||
Timeline.PaginationStatus(
|
Timeline.PaginationStatus(
|
||||||
isPaginating = false,
|
isPaginating = false,
|
||||||
hasMoreToLoad = true
|
hasMoreToLoad = true
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
private val forwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
|
override val forwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
|
||||||
Timeline.PaginationStatus(
|
Timeline.PaginationStatus(
|
||||||
isPaginating = false,
|
isPaginating = false,
|
||||||
hasMoreToLoad = false
|
hasMoreToLoad = false
|
||||||
|
|
@ -377,13 +376,6 @@ class FakeTimeline(
|
||||||
|
|
||||||
override suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> = paginateLambda(direction)
|
override suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> = paginateLambda(direction)
|
||||||
|
|
||||||
override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow<Timeline.PaginationStatus> {
|
|
||||||
return when (direction) {
|
|
||||||
Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus
|
|
||||||
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var loadReplyDetailsLambda: (eventId: EventId) -> InReplyTo = {
|
var loadReplyDetailsLambda: (eventId: EventId) -> InReplyTo = {
|
||||||
InReplyTo.NotLoaded(it)
|
InReplyTo.NotLoaded(it)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
|
@ -56,13 +57,13 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
// To reset the store: ResetStore()
|
// To reset the store: ResetStore()
|
||||||
|
|
||||||
val isAlreadyDenied: Boolean by permissionsStore
|
val isAlreadyDenied: Boolean by remember {
|
||||||
.isPermissionDenied(permission)
|
permissionsStore.isPermissionDenied(permission)
|
||||||
.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
|
|
||||||
val isAlreadyAsked: Boolean by permissionsStore
|
val isAlreadyAsked: Boolean by remember {
|
||||||
.isPermissionAsked(permission)
|
permissionsStore.isPermissionAsked(permission)
|
||||||
.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
|
|
||||||
var permissionState: PermissionState? = null
|
var permissionState: PermissionState? = null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker {
|
||||||
fun getAvailableAnalyticsProviders(): Set<AnalyticsProvider>
|
fun getAvailableAnalyticsProviders(): Set<AnalyticsProvider>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a Flow of Boolean, true if the user has given their consent.
|
* A Flow of Boolean, true if the user has given their consent.
|
||||||
*/
|
*/
|
||||||
fun getUserConsent(): Flow<Boolean>
|
val userConsentFlow: Flow<Boolean>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the user consent value.
|
* Update the user consent value.
|
||||||
|
|
@ -29,9 +29,9 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker {
|
||||||
suspend fun setUserConsent(userConsent: Boolean)
|
suspend fun setUserConsent(userConsent: Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a Flow of Boolean, true if the user has been asked for their consent.
|
* A Flow of Boolean, true if the user has been asked for their consent.
|
||||||
*/
|
*/
|
||||||
fun didAskUserConsent(): Flow<Boolean>
|
val didAskUserConsentFlow: Flow<Boolean>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the fact that the user has been asked for their consent.
|
* Store the fact that the user has been asked for their consent.
|
||||||
|
|
@ -39,9 +39,9 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker {
|
||||||
suspend fun setDidAskUserConsent()
|
suspend fun setDidAskUserConsent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a Flow of String, used for analytics Id.
|
* A Flow of String, used for analytics Id.
|
||||||
*/
|
*/
|
||||||
fun getAnalyticsId(): Flow<String>
|
val analyticsIdFlow: Flow<String>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update analyticsId from the AccountData.
|
* Update analyticsId from the AccountData.
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,10 @@ class DefaultAnalyticsService @Inject constructor(
|
||||||
// Cache for the properties to send
|
// Cache for the properties to send
|
||||||
private var pendingUserProperties: UserProperties? = null
|
private var pendingUserProperties: UserProperties? = null
|
||||||
|
|
||||||
|
override val userConsentFlow: Flow<Boolean> = analyticsStore.userConsentFlow
|
||||||
|
override val didAskUserConsentFlow: Flow<Boolean> = analyticsStore.didAskUserConsentFlow
|
||||||
|
override val analyticsIdFlow: Flow<String> = analyticsStore.analyticsIdFlow
|
||||||
|
|
||||||
init {
|
init {
|
||||||
observeUserConsent()
|
observeUserConsent()
|
||||||
observeSessions()
|
observeSessions()
|
||||||
|
|
@ -52,19 +56,11 @@ class DefaultAnalyticsService @Inject constructor(
|
||||||
return analyticsProviders
|
return analyticsProviders
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUserConsent(): Flow<Boolean> {
|
|
||||||
return analyticsStore.userConsentFlow
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setUserConsent(userConsent: Boolean) {
|
override suspend fun setUserConsent(userConsent: Boolean) {
|
||||||
Timber.tag(analyticsTag.value).d("setUserConsent($userConsent)")
|
Timber.tag(analyticsTag.value).d("setUserConsent($userConsent)")
|
||||||
analyticsStore.setUserConsent(userConsent)
|
analyticsStore.setUserConsent(userConsent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didAskUserConsent(): Flow<Boolean> {
|
|
||||||
return analyticsStore.didAskUserConsentFlow
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setDidAskUserConsent() {
|
override suspend fun setDidAskUserConsent() {
|
||||||
Timber.tag(analyticsTag.value).d("setDidAskUserConsent()")
|
Timber.tag(analyticsTag.value).d("setDidAskUserConsent()")
|
||||||
analyticsStore.setDidAskUserConsent()
|
analyticsStore.setDidAskUserConsent()
|
||||||
|
|
@ -74,10 +70,6 @@ class DefaultAnalyticsService @Inject constructor(
|
||||||
analyticsStore.setDidAskUserConsent(false)
|
analyticsStore.setDidAskUserConsent(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAnalyticsId(): Flow<String> {
|
|
||||||
return analyticsStore.analyticsIdFlow
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setAnalyticsId(analyticsId: String) {
|
override suspend fun setAnalyticsId(analyticsId: String) {
|
||||||
Timber.tag(analyticsTag.value).d("setAnalyticsId($analyticsId)")
|
Timber.tag(analyticsTag.value).d("setAnalyticsId($analyticsId)")
|
||||||
analyticsStore.setAnalyticsId(analyticsId)
|
analyticsStore.setAnalyticsId(analyticsId)
|
||||||
|
|
@ -93,7 +85,7 @@ class DefaultAnalyticsService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeUserConsent() {
|
private fun observeUserConsent() {
|
||||||
getUserConsent()
|
userConsentFlow
|
||||||
.onEach { consent ->
|
.onEach { consent ->
|
||||||
Timber.tag(analyticsTag.value).d("User consent updated to $consent")
|
Timber.tag(analyticsTag.value).d("User consent updated to $consent")
|
||||||
userConsent.set(consent)
|
userConsent.set(consent)
|
||||||
|
|
|
||||||
|
|
@ -132,10 +132,10 @@ class DefaultAnalyticsServiceTest {
|
||||||
analyticsStore = store,
|
analyticsStore = store,
|
||||||
)
|
)
|
||||||
assertThat(store.userConsentFlow.first()).isFalse()
|
assertThat(store.userConsentFlow.first()).isFalse()
|
||||||
assertThat(sut.getUserConsent().first()).isFalse()
|
assertThat(sut.userConsentFlow.first()).isFalse()
|
||||||
sut.setUserConsent(true)
|
sut.setUserConsent(true)
|
||||||
assertThat(store.userConsentFlow.first()).isTrue()
|
assertThat(store.userConsentFlow.first()).isTrue()
|
||||||
assertThat(sut.getUserConsent().first()).isTrue()
|
assertThat(sut.userConsentFlow.first()).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -146,10 +146,10 @@ class DefaultAnalyticsServiceTest {
|
||||||
analyticsStore = store,
|
analyticsStore = store,
|
||||||
)
|
)
|
||||||
assertThat(store.analyticsIdFlow.first()).isEqualTo("")
|
assertThat(store.analyticsIdFlow.first()).isEqualTo("")
|
||||||
assertThat(sut.getAnalyticsId().first()).isEqualTo("")
|
assertThat(sut.analyticsIdFlow.first()).isEqualTo("")
|
||||||
sut.setAnalyticsId(AN_ID)
|
sut.setAnalyticsId(AN_ID)
|
||||||
assertThat(store.analyticsIdFlow.first()).isEqualTo(AN_ID)
|
assertThat(store.analyticsIdFlow.first()).isEqualTo(AN_ID)
|
||||||
assertThat(sut.getAnalyticsId().first()).isEqualTo(AN_ID)
|
assertThat(sut.analyticsIdFlow.first()).isEqualTo(AN_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -160,10 +160,10 @@ class DefaultAnalyticsServiceTest {
|
||||||
analyticsStore = store,
|
analyticsStore = store,
|
||||||
)
|
)
|
||||||
assertThat(store.didAskUserConsentFlow.first()).isFalse()
|
assertThat(store.didAskUserConsentFlow.first()).isFalse()
|
||||||
assertThat(sut.didAskUserConsent().first()).isFalse()
|
assertThat(sut.didAskUserConsentFlow.first()).isFalse()
|
||||||
sut.setDidAskUserConsent()
|
sut.setDidAskUserConsent()
|
||||||
assertThat(store.didAskUserConsentFlow.first()).isTrue()
|
assertThat(store.didAskUserConsentFlow.first()).isTrue()
|
||||||
assertThat(sut.didAskUserConsent().first()).isTrue()
|
assertThat(sut.didAskUserConsentFlow.first()).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,11 @@ import javax.inject.Inject
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class NoopAnalyticsService @Inject constructor() : AnalyticsService {
|
class NoopAnalyticsService @Inject constructor() : AnalyticsService {
|
||||||
override fun getAvailableAnalyticsProviders(): Set<AnalyticsProvider> = emptySet()
|
override fun getAvailableAnalyticsProviders(): Set<AnalyticsProvider> = emptySet()
|
||||||
override fun getUserConsent(): Flow<Boolean> = flowOf(false)
|
override val userConsentFlow: Flow<Boolean> = flowOf(false)
|
||||||
override suspend fun setUserConsent(userConsent: Boolean) = Unit
|
override suspend fun setUserConsent(userConsent: Boolean) = Unit
|
||||||
override fun didAskUserConsent(): Flow<Boolean> = flowOf(true)
|
override val didAskUserConsentFlow: Flow<Boolean> = flowOf(true)
|
||||||
override suspend fun setDidAskUserConsent() = Unit
|
override suspend fun setDidAskUserConsent() = Unit
|
||||||
override fun getAnalyticsId(): Flow<String> = flowOf("")
|
override val analyticsIdFlow: Flow<String> = flowOf("")
|
||||||
override suspend fun setAnalyticsId(analyticsId: String) = Unit
|
override suspend fun setAnalyticsId(analyticsId: String) = Unit
|
||||||
override suspend fun reset() = Unit
|
override suspend fun reset() = Unit
|
||||||
override fun capture(event: VectorAnalyticsEvent) = Unit
|
override fun capture(event: VectorAnalyticsEvent) = Unit
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
class FakeAnalyticsService(
|
class FakeAnalyticsService(
|
||||||
isEnabled: Boolean = false,
|
isEnabled: Boolean = false,
|
||||||
|
|
@ -22,7 +23,7 @@ class FakeAnalyticsService(
|
||||||
private val resetLambda: () -> Unit = {},
|
private val resetLambda: () -> Unit = {},
|
||||||
) : AnalyticsService {
|
) : AnalyticsService {
|
||||||
private val isEnabledFlow = MutableStateFlow(isEnabled)
|
private val isEnabledFlow = MutableStateFlow(isEnabled)
|
||||||
private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
|
override val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
|
||||||
val capturedEvents = mutableListOf<VectorAnalyticsEvent>()
|
val capturedEvents = mutableListOf<VectorAnalyticsEvent>()
|
||||||
val screenEvents = mutableListOf<VectorAnalyticsScreen>()
|
val screenEvents = mutableListOf<VectorAnalyticsScreen>()
|
||||||
val trackedErrors = mutableListOf<Throwable>()
|
val trackedErrors = mutableListOf<Throwable>()
|
||||||
|
|
@ -30,19 +31,17 @@ class FakeAnalyticsService(
|
||||||
|
|
||||||
override fun getAvailableAnalyticsProviders(): Set<AnalyticsProvider> = emptySet()
|
override fun getAvailableAnalyticsProviders(): Set<AnalyticsProvider> = emptySet()
|
||||||
|
|
||||||
override fun getUserConsent(): Flow<Boolean> = isEnabledFlow
|
override val userConsentFlow: Flow<Boolean> = isEnabledFlow.asStateFlow()
|
||||||
|
|
||||||
override suspend fun setUserConsent(userConsent: Boolean) {
|
override suspend fun setUserConsent(userConsent: Boolean) {
|
||||||
isEnabledFlow.value = userConsent
|
isEnabledFlow.value = userConsent
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didAskUserConsent(): Flow<Boolean> = didAskUserConsentFlow
|
|
||||||
|
|
||||||
override suspend fun setDidAskUserConsent() {
|
override suspend fun setDidAskUserConsent() {
|
||||||
didAskUserConsentFlow.value = true
|
didAskUserConsentFlow.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAnalyticsId(): Flow<String> = MutableStateFlow("")
|
override val analyticsIdFlow: Flow<String> = MutableStateFlow("")
|
||||||
|
|
||||||
override suspend fun setAnalyticsId(analyticsId: String) {
|
override suspend fun setAnalyticsId(analyticsId: String) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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.tests.konsist
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.lemonappdev.konsist.api.Konsist
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.withAnnotationOf
|
||||||
|
import com.lemonappdev.konsist.api.verify.assertFalse
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class KonsistFlowTest {
|
||||||
|
@Test
|
||||||
|
fun `flow must be remembered when it is collected as state`() {
|
||||||
|
// Match
|
||||||
|
// ```).collectAsState```
|
||||||
|
// and
|
||||||
|
// ```)
|
||||||
|
// .collectAsState```
|
||||||
|
val regex = "(.*)\\)(\n\\s*)*\\.collectAsState".toRegex()
|
||||||
|
|
||||||
|
Konsist
|
||||||
|
.scopeFromProject()
|
||||||
|
.functions()
|
||||||
|
.withAnnotationOf(Composable::class)
|
||||||
|
.assertFalse(
|
||||||
|
additionalMessage = "Please check that the flow is remembered when it is collected as state." +
|
||||||
|
" Only val flows can be not remembered.",
|
||||||
|
) { function ->
|
||||||
|
regex.matches(function.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue