Merge branch 'develop' into feature/fga/timeline-multi-forward

This commit is contained in:
ganfra
2026-10-01 15:23:42 +02:00
491 changed files with 1483 additions and 1141 deletions
+12 -4
View File
@@ -2,17 +2,25 @@ appId: ${MAESTRO_APP_ID}
--- ---
- tapOn: - tapOn:
id: "home_screen-settings" id: "home_screen-settings"
- tapOn:
id: "settings-user_profile"
- assertVisible: "Your account"
- tapOn: "Remove this device" - tapOn: "Remove this device"
- takeScreenshot: build/maestro/900-SignOutScreen - takeScreenshot: build/maestro/900-SignOutScreen
- back - back
- tapOn: "Remove this device" - tapOn: "Remove this device"
# Ensure pressing back cancels
- back
- tapOn: "Remove this device"
# Ensure cancel cancels # Ensure cancel cancels
- tapOn: "Cancel"
- tapOn: "Remove this device"
- tapOn:
text: "Remove this device"
index: 1
# Cancel the dialog cancels the logout
- tapOn: - tapOn:
id: "dialog-negative" id: "dialog-negative"
- tapOn: "Remove this device" - tapOn:
text: "Remove this device"
index: 1
- tapOn: - tapOn:
id: "dialog-positive" id: "dialog-positive"
- runFlow: ../assertions/assertInitDisplayed.yaml - runFlow: ../assertions/assertInitDisplayed.yaml
+1 -1
View File
@@ -1,7 +1,7 @@
Changes in Element X v26.09.4 Changes in Element X v26.09.4
============================= =============================
## Hotifx version: ## Hotfix version:
Fixes an issue with the room list not displaying all activity from unread messages. Fixes an issue with the room list not displaying all activity from unread messages.
@@ -6,12 +6,9 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.appnav.room.joined package io.element.android.appnav.room.joined
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -68,7 +67,6 @@ fun LoadingRoomNodeView(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun LoadingRoomTopBar( private fun LoadingRoomTopBar(
onBackClick: () -> Unit, onBackClick: () -> Unit,
@@ -121,7 +121,6 @@ private fun FullscreenAnnouncementHeader(
subTitle = announcement.subtitle(), subTitle = announcement.subtitle(),
iconStyle = BigIcon.Style.Default( iconStyle = BigIcon.Style.Default(
vectorIcon = announcement.icon(), vectorIcon = announcement.icon(),
usePrimaryTint = true,
), ),
) )
} }
@@ -11,7 +11,6 @@ package io.element.android.features.createroom.impl.addpeople
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -68,7 +67,6 @@ fun AddPeopleView(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun AddPeopleTopBar( private fun AddPeopleTopBar(
onSkipClick: () -> Unit, onSkipClick: () -> Unit,
@@ -24,7 +24,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -179,7 +178,6 @@ fun ConfigureRoomView(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ConfigureRoomToolbar( private fun ConfigureRoomToolbar(
isSpace: Boolean, isSpace: Boolean,
@@ -23,7 +23,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -69,7 +68,6 @@ import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AccountDeactivationView( fun AccountDeactivationView(
state: AccountDeactivationState, state: AccountDeactivationState,
@@ -15,6 +15,7 @@ import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.androidutils.json.JsonProvider import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.core.extensions.mapCatchingExceptions
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
@@ -23,6 +24,7 @@ import io.element.android.libraries.matrix.api.accountprovider.AccountProvider
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import timber.log.Timber import timber.log.Timber
@@ -31,15 +33,16 @@ import timber.log.Timber
class DefaultEnterpriseService( class DefaultEnterpriseService(
private val temporaryMatrixClientFactory: TemporaryMatrixClientFactory, private val temporaryMatrixClientFactory: TemporaryMatrixClientFactory,
private val jsonProvider: JsonProvider, private val jsonProvider: JsonProvider,
private val dispatchers: CoroutineDispatchers,
) : EnterpriseService { ) : EnterpriseService {
override suspend fun isEnterpriseUser(sessionId: SessionId) = false override suspend fun isEnterpriseUser(sessionId: SessionId) = false
override suspend fun tweakMasUrl(url: String, urlContentFetcher: ClientUrlContentFetcher) = url override suspend fun tweakMasUrl(url: String, urlContentFetcher: ClientUrlContentFetcher) = url
override fun accountProviderAllowList(): List<AccountProvider> = emptyList() override fun accountProviderAllowList(): List<AccountProvider> = emptyList()
override fun canConnectToAnyAccountProvider(): Boolean = true override fun canConnectToAnyAccountProvider(): Boolean = true
override suspend fun isAllowedToConnectToAccountProvider(accountProvider: AccountProvider) = true override suspend fun isAllowedToConnectToAccountProvider(accountProvider: AccountProvider) = true
override suspend fun isElementProEnforced(serverName: String): Boolean { override suspend fun isElementProEnforced(serverName: String): Boolean = withContext(dispatchers.io) {
val temporaryMatrixClient = temporaryMatrixClientFactory.create(serverName).getOrElse { return false } val temporaryMatrixClient = temporaryMatrixClientFactory.create(serverName).getOrElse { return@withContext false }
return temporaryMatrixClient.use { client -> temporaryMatrixClient.use { client ->
val baseUrl = serverName.ensureProtocol().removeSuffix("/") val baseUrl = serverName.ensureProtocol().removeSuffix("/")
// We'll always perform a network request here since we're not interested in any cached value. // We'll always perform a network request here since we're not interested in any cached value.
client.getUrl("$baseUrl/.well-known/element/element.json") client.getUrl("$baseUrl/.well-known/element/element.json")
@@ -21,19 +21,21 @@ import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
import io.element.android.libraries.matrix.test.accountprovider.anAccountProviderGeneric import io.element.android.libraries.matrix.test.accountprovider.anAccountProviderGeneric
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 kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.junit.Test import org.junit.Test
class DefaultEnterpriseServiceTest { class DefaultEnterpriseServiceTest {
@Test @Test
fun accountProviderAllowList() { fun accountProviderAllowList() = runTest {
val defaultEnterpriseService = createDefaultEnterpriseService() val defaultEnterpriseService = createDefaultEnterpriseService()
assertThat(defaultEnterpriseService.accountProviderAllowList()).isEmpty() assertThat(defaultEnterpriseService.accountProviderAllowList()).isEmpty()
} }
@Test @Test
fun `canConnectToAnyAccountProvider is always true`() { fun `canConnectToAnyAccountProvider is always true`() = runTest {
val defaultEnterpriseService = createDefaultEnterpriseService() val defaultEnterpriseService = createDefaultEnterpriseService()
assertThat(defaultEnterpriseService.canConnectToAnyAccountProvider()).isTrue() assertThat(defaultEnterpriseService.canConnectToAnyAccountProvider()).isTrue()
} }
@@ -132,11 +134,12 @@ class DefaultEnterpriseServiceTest {
closeLambda.assertions().isCalledOnce() closeLambda.assertions().isCalledOnce()
} }
private fun createDefaultEnterpriseService( private fun TestScope.createDefaultEnterpriseService(
client: FakeTemporaryMatrixClient = FakeTemporaryMatrixClient(), client: FakeTemporaryMatrixClient = FakeTemporaryMatrixClient(),
jsonProvider: JsonProvider = { Json } jsonProvider: JsonProvider = { Json }
) = DefaultEnterpriseService( ) = DefaultEnterpriseService(
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(createResult = { Result.success(client) }), temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(createResult = { Result.success(client) }),
jsonProvider = jsonProvider, jsonProvider = jsonProvider,
dispatchers = testCoroutineDispatchers(),
) )
} }
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -42,7 +41,6 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ChooseSelfVerificationModeView( fun ChooseSelfVerificationModeView(
state: ChooseSelfVerificationModeState, state: ChooseSelfVerificationModeState,
-2
View File
@@ -53,7 +53,6 @@ dependencies {
implementation(projects.features.announcement.api) implementation(projects.features.announcement.api)
implementation(projects.features.invite.api) implementation(projects.features.invite.api)
implementation(projects.features.networkmonitor.api) implementation(projects.features.networkmonitor.api)
implementation(projects.features.logout.api)
implementation(projects.features.leaveroom.api) implementation(projects.features.leaveroom.api)
implementation(projects.features.rageshake.api) implementation(projects.features.rageshake.api)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
@@ -67,7 +66,6 @@ dependencies {
testCommonDependencies(libs, true) testCommonDependencies(libs, true)
testImplementation(projects.features.announcement.test) testImplementation(projects.features.announcement.test)
testImplementation(projects.features.invite.test) testImplementation(projects.features.invite.test)
testImplementation(projects.features.logout.test)
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)
@@ -40,7 +40,6 @@ import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.features.leaveroom.api.LeaveRoomRenderer import io.element.android.features.leaveroom.api.LeaveRoomRenderer
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.features.reportroom.api.ReportRoomEntryPoint import io.element.android.features.reportroom.api.ReportRoomEntryPoint
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
@@ -83,7 +82,6 @@ class HomeFlowNode(
private val inviteFriendsUseCase: InviteFriendsUseCase, private val inviteFriendsUseCase: InviteFriendsUseCase,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val acceptDeclineInviteView: AcceptDeclineInviteView, private val acceptDeclineInviteView: AcceptDeclineInviteView,
private val directLogoutView: DirectLogoutView,
private val reportRoomEntryPoint: ReportRoomEntryPoint, private val reportRoomEntryPoint: ReportRoomEntryPoint,
private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint, private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint,
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint, private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
@@ -248,7 +246,6 @@ class HomeFlowNode(
) )
} }
) )
directLogoutView.Render(state.directLogoutState)
} }
} }
@@ -21,7 +21,6 @@ import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -42,7 +41,6 @@ class HomePresenter(
private val indicatorService: IndicatorService, private val indicatorService: IndicatorService,
private val roomListPresenter: Presenter<RoomListState>, private val roomListPresenter: Presenter<RoomListState>,
private val homeSpacesPresenter: Presenter<HomeSpacesState>, private val homeSpacesPresenter: Presenter<HomeSpacesState>,
private val logoutPresenter: Presenter<DirectLogoutState>,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
) : Presenter<HomeState> { ) : Presenter<HomeState> {
@@ -75,7 +73,6 @@ class HomePresenter(
} }
// Avatar indicator // Avatar indicator
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator() val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
val directLogoutState = logoutPresenter.present()
fun handleEvent(event: HomeEvent) { fun handleEvent(event: HomeEvent) {
when (event) { when (event) {
@@ -98,7 +95,6 @@ class HomePresenter(
homeSpacesState = homeSpacesState, homeSpacesState = homeSpacesState,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
canReportBug = canReportBug, canReportBug = canReportBug,
directLogoutState = directLogoutState,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
@@ -11,7 +11,6 @@ package io.element.android.features.home.impl
import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@@ -29,7 +28,6 @@ data class HomeState(
val homeSpacesState: HomeSpacesState, val homeSpacesState: HomeSpacesState,
val snackbarMessage: SnackbarMessage?, val snackbarMessage: SnackbarMessage?,
val canReportBug: Boolean, val canReportBug: Boolean,
val directLogoutState: DirectLogoutState,
val eventSink: (HomeEvent) -> Unit, val eventSink: (HomeEvent) -> Unit,
) { ) {
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
@@ -16,8 +16,6 @@ import io.element.android.features.home.impl.roomlist.aRoomsContentState
import io.element.android.features.home.impl.roomlist.generateRoomListRoomSummaryList import io.element.android.features.home.impl.roomlist.generateRoomListRoomSummaryList
import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.home.impl.spaces.aHomeSpacesState import io.element.android.features.home.impl.spaces.aHomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
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.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -63,7 +61,6 @@ internal fun aHomeState(
roomListState: RoomListState = aRoomListState(), roomListState: RoomListState = aRoomListState(),
homeSpacesState: HomeSpacesState = aHomeSpacesState(), homeSpacesState: HomeSpacesState = aHomeSpacesState(),
canReportBug: Boolean = true, canReportBug: Boolean = true,
directLogoutState: DirectLogoutState = aDirectLogoutState(),
eventSink: (HomeEvent) -> Unit = {} eventSink: (HomeEvent) -> Unit = {}
) = HomeState( ) = HomeState(
currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(), currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
@@ -71,7 +68,6 @@ internal fun aHomeState(
hasNetworkConnection = hasNetworkConnection, hasNetworkConnection = hasNetworkConnection,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
canReportBug = canReportBug, canReportBug = canReportBug,
directLogoutState = directLogoutState,
currentHomeNavigationBarItem = currentHomeNavigationBarItem, currentHomeNavigationBarItem = currentHomeNavigationBarItem,
roomListState = roomListState, roomListState = roomListState,
homeSpacesState = homeSpacesState, homeSpacesState = homeSpacesState,
@@ -22,8 +22,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.plus import androidx.compose.foundation.layout.plus
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
@@ -150,7 +148,6 @@ fun HomeView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun HomeScaffold( private fun HomeScaffold(
state: HomeState, state: HomeState,
@@ -187,7 +184,7 @@ private fun HomeScaffold(
val spacesLazyListState = rememberLazyListState() val spacesLazyListState = rememberLazyListState()
Scaffold( Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = modifier,
topBar = { topBar = {
HomeTopBar( HomeTopBar(
selectedNavigationItem = state.currentHomeNavigationBarItem, selectedNavigationItem = state.currentHomeNavigationBarItem,
@@ -268,42 +265,50 @@ private fun HomeScaffold(
val contentPadding = PaddingValues( val contentPadding = PaddingValues(
bottom = 96.dp, bottom = 96.dp,
) )
when (state.currentHomeNavigationBarItem) { // The nested scroll connection is set on the content and not on the Scaffold, else the top bar would
HomeNavigationBarItem.Chats -> { // consume the scroll events of its own content.
RoomListContentView( Box(
contentState = roomListState.contentState, modifier = Modifier
filtersState = roomListState.filtersState, .fillMaxSize()
spaceFiltersState = roomListState.spaceFiltersState, .nestedScroll(scrollBehavior.nestedScrollConnection)
lazyListState = roomsLazyListState, ) {
hideInvitesAvatars = roomListState.hideInvitesAvatars, when (state.currentHomeNavigationBarItem) {
eventSink = roomListState.eventSink, HomeNavigationBarItem.Chats -> {
onSetUpRecoveryClick = onSetUpRecoveryClick, RoomListContentView(
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, contentState = roomListState.contentState,
onRoomClick = ::onRoomClick, filtersState = roomListState.filtersState,
onCreateRoomClick = onStartChatClick, spaceFiltersState = roomListState.spaceFiltersState,
contentPadding = lazyColumnContentPadding + contentPadding, lazyListState = roomsLazyListState,
modifier = Modifier hideInvitesAvatars = roomListState.hideInvitesAvatars,
.padding(outerPadding) eventSink = roomListState.eventSink,
.consumeWindowInsets(outerPadding) onSetUpRecoveryClick = onSetUpRecoveryClick,
) onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
SpaceFiltersView(roomListState.spaceFiltersState) onRoomClick = ::onRoomClick,
} onCreateRoomClick = onStartChatClick,
HomeNavigationBarItem.Spaces -> { contentPadding = lazyColumnContentPadding + contentPadding,
HomeSpacesView( modifier = Modifier
modifier = Modifier .padding(outerPadding)
.fillMaxSize() .consumeWindowInsets(outerPadding)
.padding(outerPadding) )
.consumeWindowInsets(outerPadding), SpaceFiltersView(roomListState.spaceFiltersState)
contentPadding = lazyColumnContentPadding + contentPadding, }
state = state.homeSpacesState, HomeNavigationBarItem.Spaces -> {
lazyListState = spacesLazyListState, HomeSpacesView(
onSpaceClick = { spaceId -> modifier = Modifier
onRoomClick(spaceId) .fillMaxSize()
}, .padding(outerPadding)
onCreateSpaceClick = onCreateSpaceClick, .consumeWindowInsets(outerPadding),
// TODO use actual callbacks for this contentPadding = lazyColumnContentPadding + contentPadding,
onExploreClick = {}, state = state.homeSpacesState,
) lazyListState = spacesLazyListState,
onSpaceClick = { spaceId ->
onRoomClick(spaceId)
},
onCreateSpaceClick = onCreateSpaceClick,
// TODO use actual callbacks for this
onExploreClick = {},
)
}
} }
} }
}, },
@@ -325,7 +330,6 @@ private fun HomeFloatingActionButton(
} }
} }
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
private fun HomeBottomBar( private fun HomeBottomBar(
currentHomeNavigationBarItem: HomeNavigationBarItem, currentHomeNavigationBarItem: HomeNavigationBarItem,
@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
@@ -100,7 +99,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HomeTopBar( fun HomeTopBar(
selectedNavigationItem: HomeNavigationBarItem, selectedNavigationItem: HomeNavigationBarItem,
@@ -449,7 +447,6 @@ private fun StatusEmojiBadge(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun HomeTopBarPreview() = ElementPreview { internal fun HomeTopBarPreview() = ElementPreview {
@@ -470,7 +467,6 @@ internal fun HomeTopBarPreview() = ElementPreview {
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview { internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview {
@@ -491,7 +487,6 @@ internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview {
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun HomeTopBarSpacesPreview() = ElementPreview { internal fun HomeTopBarSpacesPreview() = ElementPreview {
@@ -512,7 +507,6 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview {
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun HomeTopBarWithIndicatorPreview() = ElementPreview { internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
@@ -533,7 +527,6 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun HomeTopBarWithStatusPreview() = ElementPreview { internal fun HomeTopBarWithStatusPreview() = ElementPreview {
@@ -560,7 +553,6 @@ internal fun HomeTopBarWithStatusPreview() = ElementPreview {
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun HomeTopBarMultiAccountPreview() = ElementPreview { internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
@@ -26,7 +26,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -88,7 +87,6 @@ internal fun RoomListSearchView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun RoomListSearchContent( private fun RoomListSearchContent(
state: RoomListSearchState, state: RoomListSearchState,
@@ -36,7 +36,6 @@ class DefaultHomeEntryPointTest : RobolectricTest() {
inviteFriendsUseCase = { lambdaError() }, inviteFriendsUseCase = { lambdaError() },
analyticsService = FakeAnalyticsService(), analyticsService = FakeAnalyticsService(),
acceptDeclineInviteView = { _, _, _, _ -> lambdaError() }, acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
directLogoutView = { _ -> lambdaError() },
reportRoomEntryPoint = { _, _, _ -> lambdaError() }, reportRoomEntryPoint = { _, _, _ -> lambdaError() },
declineInviteAndBlockUserEntryPoint = FakeDeclineInviteAndBlockEntryPoint(), declineInviteAndBlockUserEntryPoint = FakeDeclineInviteAndBlockEntryPoint(),
changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() }, changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() },
@@ -12,7 +12,6 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.home.impl.roomlist.aRoomListState import io.element.android.features.home.impl.roomlist.aRoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.home.impl.spaces.aHomeSpacesState import io.element.android.features.home.impl.spaces.aHomeSpacesState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -162,7 +161,6 @@ internal fun createHomePresenter(
indicatorService = indicatorService, indicatorService = indicatorService,
roomListPresenter = { aRoomListState() }, roomListPresenter = { aRoomListState() },
homeSpacesPresenter = homeSpacesPresenter, homeSpacesPresenter = homeSpacesPresenter,
logoutPresenter = { aDirectLogoutState() },
rageshakeFeatureAvailability = rageshakeFeatureAvailability, rageshakeFeatureAvailability = rageshakeFeatureAvailability,
sessionStore = sessionStore, sessionStore = sessionStore,
) )
@@ -217,6 +217,10 @@ class RoomListPresenterTest {
val presenter = createRoomListPresenter(client = client) val presenter = createRoomListPresenter(client = client)
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
// Skip intermediate event for loading the value of the `UnreadIndicatorCount` feature flag
skipItems(1)
val summary = createRoomListRoomSummary() val summary = createRoomListRoomSummary()
initialState.eventSink(RoomListEvent.ShowContextMenu(summary)) initialState.eventSink(RoomListEvent.ShowContextMenu(summary))
@@ -260,6 +264,10 @@ class RoomListPresenterTest {
val presenter = createRoomListPresenter(client = client) val presenter = createRoomListPresenter(client = client)
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
// Skip intermediate event for loading the value of the `UnreadIndicatorCount` feature flag
skipItems(1)
val summary = createRoomListRoomSummary() val summary = createRoomListRoomSummary()
initialState.eventSink(RoomListEvent.ShowContextMenu(summary)) initialState.eventSink(RoomListEvent.ShowContextMenu(summary))
@@ -309,6 +317,10 @@ class RoomListPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
// Skip intermediate event for loading the value of the `UnreadIndicatorCount` feature flag
skipItems(1)
eventRecorder.assertEmpty() eventRecorder.assertEmpty()
initialState.eventSink(RoomListEvent.ToggleSearchResults) initialState.eventSink(RoomListEvent.ToggleSearchResults)
eventRecorder.assertSingle( eventRecorder.assertSingle(
@@ -398,6 +410,7 @@ class RoomListPresenterTest {
client = matrixClient, client = matrixClient,
) )
presenter.test { presenter.test {
skipItems(1) // Skip initial state
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java) assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java)
} }
} }
@@ -616,7 +629,7 @@ class RoomListPresenterTest {
) )
presenter.test { presenter.test {
assertThat(announcementService.announcementsToShowFlow().first()).isEmpty() assertThat(announcementService.announcementsToShowFlow().first()).isEmpty()
skipItems(1) skipItems(2) // Skip initial state and intermediate event for loading the value of the `UnreadIndicatorCount` feature flag
val state = awaitItem() val state = awaitItem()
assertThat(state.contentAsRooms().showNewNotificationSoundBanner).isFalse() assertThat(state.contentAsRooms().showNewNotificationSoundBanner).isFalse()
announcementService.emitAnnouncementsToShow(listOf(Announcement.NewNotificationSound)) announcementService.emitAnnouncementsToShow(listOf(Announcement.NewNotificationSound))
@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
@@ -41,7 +40,6 @@ import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DeclineAndBlockView( fun DeclineAndBlockView(
state: DeclineAndBlockState, state: DeclineAndBlockState,
@@ -171,7 +171,6 @@ private fun InvitePeopleContentView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun InvitePeopleSearchBar( private fun InvitePeopleSearchBar(
queryState: TextFieldState, queryState: TextFieldState,
@@ -24,7 +24,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -593,7 +592,6 @@ private fun DefaultLoadedContent(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun JoinRoomTopBar( private fun JoinRoomTopBar(
contentState: ContentState, contentState: ContentState,
@@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@@ -479,7 +478,6 @@ private fun KnockRequestsEmptyList(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun KnockRequestsListTopBar(onBackClick: () -> Unit) { private fun KnockRequestsListTopBar(onBackClick: () -> Unit) {
TopAppBar( TopAppBar(
@@ -12,7 +12,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.plus import androidx.compose.foundation.layout.plus
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -28,7 +27,6 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DependenciesDetailsView( fun DependenciesDetailsView(
licenseItem: DependencyLicenseItem, licenseItem: DependencyLicenseItem,
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.plus import androidx.compose.foundation.layout.plus
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -39,7 +38,6 @@ import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DependencyLicensesListView( fun DependencyLicensesListView(
state: DependencyLicensesListState, state: DependencyLicensesListState,
@@ -5,8 +5,6 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.linknewdevice.impl.screens.desktop package io.element.android.features.linknewdevice.impl.screens.desktop
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -16,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -5,8 +5,6 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.linknewdevice.impl.screens.number package io.element.android.features.linknewdevice.impl.screens.number
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -16,7 +14,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -5,8 +5,6 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.linknewdevice.impl.screens.qrcode package io.element.android.features.linknewdevice.impl.screens.qrcode
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
@@ -21,7 +19,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -5,12 +5,9 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.linknewdevice.impl.screens.root package io.element.android.features.linknewdevice.impl.screens.root
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -6,8 +6,6 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.lockscreen.impl.setup.pin package io.element.android.features.lockscreen.impl.setup.pin
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -17,7 +15,6 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
@@ -12,7 +12,10 @@ package io.element.android.features.lockscreen.impl.unlock.keypad
import android.view.KeyEvent import android.view.KeyEvent
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.input.InputMode
import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
import androidx.compose.ui.platform.LocalInputModeManager
import androidx.compose.ui.test.AndroidComposeUiTest import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasContentDescription
@@ -52,7 +55,9 @@ class PinKeypadTest : RobolectricTest() {
@Test @Test
fun `typing using the hardware keyboard emits the expected events`() = runAndroidComposeUiTest { fun `typing using the hardware keyboard emits the expected events`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PinKeypadModel>() val eventsRecorder = EventsRecorder<PinKeypadModel>()
setPinKeyPad(onClick = eventsRecorder) val inputModeManager = setPinKeyPad(onClick = eventsRecorder)
// Like a hardware keyboard would do, leave the touch mode, else the keys are not focusable.
runOnIdle { inputModeManager.requestInputMode(InputMode.Keyboard) }
onNodeWithText("1").requestFocus() onNodeWithText("1").requestFocus()
onAllNodes(isRoot())[0].performKeyInput { onAllNodes(isRoot())[0].performKeyInput {
val keys = listOf( val keys = listOf(
@@ -115,13 +120,16 @@ class PinKeypadTest : RobolectricTest() {
private fun AndroidComposeUiTest<ComponentActivity>.setPinKeyPad( private fun AndroidComposeUiTest<ComponentActivity>.setPinKeyPad(
onClick: (PinKeypadModel) -> Unit = EnsureNeverCalledWithParam(), onClick: (PinKeypadModel) -> Unit = EnsureNeverCalledWithParam(),
) { ): InputModeManager {
lateinit var inputModeManager: InputModeManager
setContent { setContent {
inputModeManager = LocalInputModeManager.current
PinKeypad( PinKeypad(
onClick = onClick, onClick = onClick,
maxWidth = 1000.dp, maxWidth = 1000.dp,
maxHeight = 1000.dp, maxHeight = 1000.dp,
) )
} }
return inputModeManager
} }
} }
@@ -25,6 +25,7 @@ import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
import io.element.android.features.login.impl.BuildConfig import io.element.android.features.login.impl.BuildConfig
import io.element.android.libraries.androidutils.service.ServiceBinder import io.element.android.libraries.androidutils.service.ServiceBinder
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
@@ -69,6 +70,7 @@ class DefaultElementClassicConnection(
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val matrixAuthenticationService: MatrixAuthenticationService, private val matrixAuthenticationService: MatrixAuthenticationService,
private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker, private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker,
private val dispatchers: CoroutineDispatchers,
) : ElementClassicConnection { ) : ElementClassicConnection {
// Messenger for communicating with the service. // Messenger for communicating with the service.
private var messenger: Messenger? = null private var messenger: Messenger? = null
@@ -221,7 +223,7 @@ class DefaultElementClassicConnection(
fun onSessionReceived(data: Bundle) { fun onSessionReceived(data: Bundle) {
// The data must be extracted from the bundle before we launch the coroutine, else the bundle will be emptied // The data must be extracted from the bundle before we launch the coroutine, else the bundle will be emptied
val state = data.toElementClassicConnectionState() val state = data.toElementClassicConnectionState()
coroutineScope.launch { coroutineScope.launch(dispatchers.io) {
val updatedState = ensureHomeserverIsSupported(state) val updatedState = ensureHomeserverIsSupported(state)
emitState(updatedState) emitState(updatedState)
val userId = (updatedState as? ElementClassicConnectionState.ElementClassicReady)?.elementClassicSession?.userId val userId = (updatedState as? ElementClassicConnectionState.ElementClassicReady)?.elementClassicSession?.userId
@@ -6,8 +6,6 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.login.impl.screens.chooseaccountprovider package io.element.android.features.login.impl.screens.chooseaccountprovider
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -21,7 +19,6 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -56,7 +55,6 @@ import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LoginWithClassicView( fun LoginWithClassicView(
state: LoginWithClassicState, state: LoginWithClassicState,
@@ -23,7 +23,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -70,7 +69,6 @@ import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LoginPasswordView( fun LoginPasswordView(
state: LoginPasswordState, state: LoginPasswordState,
@@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationSer
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.robolectric.RobolectricTest import io.element.android.tests.testutils.robolectric.RobolectricTest
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@@ -499,5 +500,6 @@ class DefaultElementClassicConnectionTest : RobolectricTest() {
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
matrixAuthenticationService = matrixAuthenticationService, matrixAuthenticationService = matrixAuthenticationService,
homeServerLoginCompatibilityChecker = homeServerLoginCompatibilityChecker, homeServerLoginCompatibilityChecker = homeServerLoginCompatibilityChecker,
dispatchers = testCoroutineDispatchers(),
) )
} }
@@ -11,7 +11,6 @@ package io.element.android.features.logout.api.direct
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
data class DirectLogoutState( data class DirectLogoutState(
val canDoDirectSignOut: Boolean,
val logoutAction: AsyncAction<Unit>, val logoutAction: AsyncAction<Unit>,
val eventSink: (DirectLogoutEvent) -> Unit, val eventSink: (DirectLogoutEvent) -> Unit,
) )
@@ -23,11 +23,9 @@ open class DirectLogoutStatePreviewParam : PreviewParameterProvider<DirectLogout
} }
fun aDirectLogoutState( fun aDirectLogoutState(
canDoDirectSignOut: Boolean = true,
logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized, logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (DirectLogoutEvent) -> Unit = {}, eventSink: (DirectLogoutEvent) -> Unit = {},
) = DirectLogoutState( ) = DirectLogoutState(
canDoDirectSignOut = canDoDirectSignOut,
logoutAction = logoutAction, logoutAction = logoutAction,
eventSink = eventSink, eventSink = eventSink,
) )
+1
View File
@@ -31,6 +31,7 @@ dependencies {
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.testtags) implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
@@ -41,6 +41,7 @@ class LogoutPresenter(
@Composable @Composable
override fun present(): LogoutState { override fun present(): LogoutState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val currentUser by matrixClient.userProfile.collectAsState()
val logoutAction: MutableState<AsyncAction<Unit>> = remember { val logoutAction: MutableState<AsyncAction<Unit>> = remember {
mutableStateOf(AsyncAction.Uninitialized) mutableStateOf(AsyncAction.Uninitialized)
} }
@@ -90,6 +91,7 @@ class LogoutPresenter(
} }
return LogoutState( return LogoutState(
currentUser = currentUser,
isLastDevice = isLastDevice, isLastDevice = isLastDevice,
backupState = backupState, backupState = backupState,
doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(), doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(),
@@ -12,8 +12,10 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
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.user.MatrixUser
data class LogoutState( data class LogoutState(
val currentUser: MatrixUser,
val isLastDevice: Boolean, val isLastDevice: Boolean,
val backupState: BackupState, val backupState: BackupState,
val doesBackupExistOnServer: Boolean, val doesBackupExistOnServer: Boolean,
@@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
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.encryption.SteadyStateException import io.element.android.libraries.matrix.api.encryption.SteadyStateException
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUser
open class LogoutStatePreviewParam : PreviewParameterProvider<LogoutState> { open class LogoutStatePreviewParam : PreviewParameterProvider<LogoutState> {
override val values: Sequence<LogoutState> override val values: Sequence<LogoutState>
@@ -39,10 +41,12 @@ open class LogoutStatePreviewParam : PreviewParameterProvider<LogoutState> {
backupUploadState = BackupUploadState.Waiting, backupUploadState = BackupUploadState.Waiting,
waitingForALongTime = true, waitingForALongTime = true,
), ),
aLogoutState(currentUser = aMatrixUser(id = "@alice:server.org", displayName = null)),
) )
} }
fun aLogoutState( fun aLogoutState(
currentUser: MatrixUser = aMatrixUser(),
isLastDevice: Boolean = false, isLastDevice: Boolean = false,
backupState: BackupState = BackupState.ENABLED, backupState: BackupState = BackupState.ENABLED,
doesBackupExistOnServer: Boolean = true, doesBackupExistOnServer: Boolean = true,
@@ -52,6 +56,7 @@ fun aLogoutState(
logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized, logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (LogoutEvent) -> Unit = {}, eventSink: (LogoutEvent) -> Unit = {},
) = LogoutState( ) = LogoutState(
currentUser = currentUser,
isLastDevice = isLastDevice, isLastDevice = isLastDevice,
backupState = backupState, backupState = backupState,
doesBackupExistOnServer = doesBackupExistOnServer, doesBackupExistOnServer = doesBackupExistOnServer,
@@ -11,7 +11,9 @@ package io.element.android.features.logout.impl
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -32,15 +34,22 @@ import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
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.encryption.SteadyStateException import io.element.android.libraries.matrix.api.encryption.SteadyStateException
import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCard
import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCardMode
import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCardState
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
/**
* Ref: https://www.figma.com/design/kEAcfun9iSpszeUDvdKZ6b/ER-351--Multi-account-in-EX?node-id=442-34581
*/
@Composable @Composable
fun LogoutView( fun LogoutView(
state: LogoutState, state: LogoutState,
@@ -54,15 +63,17 @@ fun LogoutView(
onBackClick = onBackClick, onBackClick = onBackClick,
title = title(state), title = title(state),
subTitle = subtitle(state), subTitle = subtitle(state),
iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()), iconStyle = BigIcon.Style.Default(CompoundIcons.Mobile()),
modifier = modifier, modifier = modifier,
isScrollable = true,
buttons = { buttons = {
Buttons( Buttons(
state = state, state = state,
onChangeRecoveryKeyClick = onChangeRecoveryKeyClick, onChangeRecoveryKeyClick = onChangeRecoveryKeyClick,
onLogoutClick = { onLogoutClick = {
eventSink(LogoutEvent.Logout(ignoreSdkError = false)) eventSink(LogoutEvent.Logout(ignoreSdkError = false))
} },
onCancelClick = onBackClick,
) )
}, },
) { ) {
@@ -71,6 +82,8 @@ fun LogoutView(
LogoutActionDialog( LogoutActionDialog(
state.logoutAction, state.logoutAction,
// Do not show the progress dialog, the progress is rendered in the button.
showProgressDialog = false,
onConfirmClick = { onConfirmClick = {
eventSink(LogoutEvent.Logout(ignoreSdkError = false)) eventSink(LogoutEvent.Logout(ignoreSdkError = false))
}, },
@@ -116,6 +129,7 @@ private fun ColumnScope.Buttons(
state: LogoutState, state: LogoutState,
onLogoutClick: () -> Unit, onLogoutClick: () -> Unit,
onChangeRecoveryKeyClick: () -> Unit, onChangeRecoveryKeyClick: () -> Unit,
onCancelClick: () -> Unit,
) { ) {
val logoutAction = state.logoutAction val logoutAction = state.logoutAction
if (state.isLastDevice) { if (state.isLastDevice) {
@@ -139,6 +153,12 @@ private fun ColumnScope.Buttons(
.testTag(TestTags.signOut), .testTag(TestTags.signOut),
onClick = onLogoutClick, onClick = onLogoutClick,
) )
TextButton(
text = stringResource(id = CommonStrings.action_cancel),
enabled = logoutAction !is AsyncAction.Loading,
modifier = Modifier.fillMaxWidth(),
onClick = onCancelClick,
)
} }
@Composable @Composable
@@ -149,9 +169,16 @@ private fun Content(
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 60.dp, start = 20.dp, end = 20.dp), .padding(top = 8.dp, start = 16.dp, end = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
AccountInfoCard(
AccountInfoCardState(
matrixUser = state.currentUser,
mode = AccountInfoCardMode.Simple,
)
)
Spacer(modifier = Modifier.height(16.dp))
when (state.backupUploadState) { when (state.backupUploadState) {
is BackupUploadState.Uploading -> { is BackupUploadState.Uploading -> {
LinearProgressIndicator( LinearProgressIndicator(
@@ -27,6 +27,7 @@ class DefaultDirectLogoutView : DirectLogoutView {
val eventSink = state.eventSink val eventSink = state.eventSink
LogoutActionDialog( LogoutActionDialog(
state.logoutAction, state.logoutAction,
showProgressDialog = true,
onConfirmClick = { onConfirmClick = {
eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false)) eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false))
}, },
@@ -10,28 +10,22 @@ package io.element.android.features.logout.impl.direct
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.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
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.logout.api.direct.DirectLogoutEvent import io.element.android.features.logout.api.direct.DirectLogoutEvent
import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.impl.tools.isBackingUp
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Inject @Inject
class DirectLogoutPresenter( class DirectLogoutPresenter(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val encryptionService: EncryptionService,
) : Presenter<DirectLogoutState> { ) : Presenter<DirectLogoutState> {
@Composable @Composable
override fun present(): DirectLogoutState { override fun present(): DirectLogoutState {
@@ -41,13 +35,6 @@ class DirectLogoutPresenter(
mutableStateOf(AsyncAction.Uninitialized) mutableStateOf(AsyncAction.Uninitialized)
} }
val backupUploadState: BackupUploadState by remember {
encryptionService.waitForBackupUploadSteadyState()
}
.collectAsState(initial = BackupUploadState.Unknown)
val isLastDevice by encryptionService.isLastDevice.collectAsState()
fun handleEvent(event: DirectLogoutEvent) { fun handleEvent(event: DirectLogoutEvent) {
when (event) { when (event) {
is DirectLogoutEvent.Logout -> { is DirectLogoutEvent.Logout -> {
@@ -64,8 +51,6 @@ class DirectLogoutPresenter(
} }
return DirectLogoutState( return DirectLogoutState(
canDoDirectSignOut = !isLastDevice &&
!backupUploadState.isBackingUp(),
logoutAction = logoutAction.value, logoutAction = logoutAction.value,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
@@ -19,6 +19,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun LogoutActionDialog( fun LogoutActionDialog(
state: AsyncAction<Unit>, state: AsyncAction<Unit>,
showProgressDialog: Boolean,
onConfirmClick: () -> Unit, onConfirmClick: () -> Unit,
onForceLogoutClick: () -> Unit, onForceLogoutClick: () -> Unit,
onDismissDialog: () -> Unit, onDismissDialog: () -> Unit,
@@ -31,8 +32,11 @@ fun LogoutActionDialog(
onSubmitClick = onConfirmClick, onSubmitClick = onConfirmClick,
onDismiss = onDismissDialog onDismiss = onDismissDialog
) )
is AsyncAction.Loading -> is AsyncAction.Loading -> {
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) if (showProgressDialog) {
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
}
}
is AsyncAction.Failure -> is AsyncAction.Failure ->
RetryDialog( RetryDialog(
title = stringResource(id = CommonStrings.dialog_title_error), title = stringResource(id = CommonStrings.dialog_title_error),
@@ -20,7 +20,9 @@ import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService 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.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
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.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.workmanager.api.WorkManagerRequestType import io.element.android.libraries.workmanager.api.WorkManagerRequestType
@@ -39,11 +41,18 @@ class LogoutPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = createLogoutPresenter() val presenter = createLogoutPresenter(
matrixClient = FakeMatrixClient(
sessionId = A_SESSION_ID,
userDisplayName = "Alice",
userAvatarUrl = "mxc://avatar",
),
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.currentUser).isEqualTo(MatrixUser(A_SESSION_ID, "Alice", "mxc://avatar"))
assertThat(initialState.isLastDevice).isFalse() assertThat(initialState.isLastDevice).isFalse()
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN) assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
assertThat(initialState.doesBackupExistOnServer).isTrue() assertThat(initialState.doesBackupExistOnServer).isTrue()
@@ -13,8 +13,11 @@ package io.element.android.features.logout.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.AndroidComposeUiTest import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@@ -25,8 +28,22 @@ import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressTag import io.element.android.tests.testutils.pressTag
import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.robolectric.RobolectricTest
import org.junit.Test import org.junit.Test
import org.robolectric.annotation.Config
class LogoutViewTest : RobolectricTest() { class LogoutViewTest : RobolectricTest() {
@Test
@Config(qualifiers = "h1024dp")
fun `the current user is displayed`() = runAndroidComposeUiTest {
setLogoutView(
aLogoutState(
currentUser = aMatrixUser(id = "@alice:server.org", displayName = "Alice"),
eventSink = EventsRecorder(expectEvents = false),
),
)
onNodeWithText("Alice").assertIsDisplayed()
onNodeWithText("@alice:server.org").assertIsDisplayed()
}
@Test @Test
fun `clicking on logout sends a LogoutEvent`() = runAndroidComposeUiTest { fun `clicking on logout sends a LogoutEvent`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LogoutEvent>() val eventsRecorder = EventsRecorder<LogoutEvent>()
@@ -88,10 +105,24 @@ class LogoutViewTest : RobolectricTest() {
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel, inDialog = true)
eventsRecorder.assertSingle(LogoutEvent.CloseDialogs) eventsRecorder.assertSingle(LogoutEvent.CloseDialogs)
} }
@Test
fun `clicking on cancel invoke back callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LogoutEvent>(expectEvents = false)
ensureCalledOnce { callback ->
setLogoutView(
aLogoutState(
eventSink = eventsRecorder
),
onBackClick = callback,
)
clickOn(CommonStrings.action_cancel)
}
}
@Test @Test
fun `last session setting button invoke onChangeRecoveryKeyClicked`() = runAndroidComposeUiTest { fun `last session setting button invoke onChangeRecoveryKeyClicked`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LogoutEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<LogoutEvent>(expectEvents = false)
@@ -16,13 +16,9 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.DirectLogoutEvent import io.element.android.features.logout.api.direct.DirectLogoutEvent
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.flow.flow
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
@@ -38,44 +34,6 @@ class DirectLogoutPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.canDoDirectSignOut).isTrue()
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@Test
fun `present - initial state - last session`() = runTest {
val presenter = createDirectLogoutPresenter(
encryptionService = FakeEncryptionService().apply {
emitIsLastDevice(true)
}
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.canDoDirectSignOut).isFalse()
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@Test
fun `present - initial state - backing up`() = runTest {
val encryptionService = FakeEncryptionService()
encryptionService.givenWaitForBackupUploadSteadyStateFlow(
flow {
emit(BackupUploadState.Waiting)
}
)
val presenter = createDirectLogoutPresenter(
encryptionService = encryptionService
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitFirstItem()
assertThat(initialState.canDoDirectSignOut).isFalse()
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
} }
} }
@@ -180,9 +138,7 @@ class DirectLogoutPresenterTest {
private fun createDirectLogoutPresenter( private fun createDirectLogoutPresenter(
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(),
encryptionService: EncryptionService = FakeEncryptionService(),
): DirectLogoutPresenter = DirectLogoutPresenter( ): DirectLogoutPresenter = DirectLogoutPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
encryptionService = encryptionService,
) )
} }
@@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -92,7 +91,6 @@ import kotlin.time.Duration.Companion.milliseconds
/** /**
* Ref: https://www.figma.com/design/zftpgS6LjiczobJZ1GUNpt/Updates-to-Media---File-Upload?node-id=51-3514 * Ref: https://www.figma.com/design/zftpgS6LjiczobJZ1GUNpt/Updates-to-Media---File-Upload?node-id=51-3514
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AttachmentsPreviewView( fun AttachmentsPreviewView(
state: AttachmentsPreviewState, state: AttachmentsPreviewState,
@@ -263,7 +261,7 @@ private fun AttachmentSendStateView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun AttachmentPreviewContent( private fun AttachmentPreviewContent(
state: AttachmentsPreviewState, state: AttachmentsPreviewState,
@@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -82,7 +81,6 @@ private val maxHandleTouchRadius = 56.dp
/** /**
* Ref: https://www.figma.com/design/zftpgS6LjiczobJZ1GUNpt/Updates-to-Media---File-Upload?node-id=51-3539 * Ref: https://www.figma.com/design/zftpgS6LjiczobJZ1GUNpt/Updates-to-Media---File-Upload?node-id=51-3539
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AttachmentImageEditorView( fun AttachmentImageEditorView(
state: AttachmentImageEditorState, state: AttachmentImageEditorState,
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.plus
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -107,7 +106,6 @@ fun PinnedMessagesListView(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun PinnedMessagesListTopBar( private fun PinnedMessagesListTopBar(
state: PinnedMessagesListState, state: PinnedMessagesListState,
@@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -43,7 +42,6 @@ import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ReportMessageView( fun ReportMessageView(
state: ReportMessageState, state: ReportMessageState,
@@ -23,7 +23,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@@ -74,7 +73,6 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ThreadsListView( fun ThreadsListView(
state: ThreadsListState, state: ThreadsListState,
@@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -57,7 +56,6 @@ import org.json.JSONObject
* Screen used to display debug info for events. * Screen used to display debug info for events.
* It will only be available in debug builds. * It will only be available in debug builds.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun EventDebugInfoView( fun EventDebugInfoView(
eventId: EventId?, eventId: EventId?,
@@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -61,7 +60,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
internal fun MessagesViewTopBar( internal fun MessagesViewTopBar(
roomName: String?, roomName: String?,
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -43,7 +42,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
internal fun ThreadTopBar( internal fun ThreadTopBar(
roomName: String?, roomName: String?,
@@ -22,7 +22,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -216,7 +215,6 @@ fun CreatePollView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun CreatePollTopAppBar( private fun CreatePollTopAppBar(
mode: CreatePollState.Mode, mode: CreatePollState.Mode,
@@ -23,7 +23,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -53,7 +52,6 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun PollHistoryView( fun PollHistoryView(
state: PollHistoryState, state: PollHistoryState,
@@ -125,7 +123,6 @@ fun PollHistoryView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun PollHistoryFilterButtons( private fun PollHistoryFilterButtons(
activeFilter: PollHistoryFilter, activeFilter: PollHistoryFilter,
@@ -17,7 +17,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -34,7 +33,6 @@ class PreferencesAccountPresenter(
private val sessionVerificationService: SessionVerificationService, private val sessionVerificationService: SessionVerificationService,
private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,
private val indicatorService: IndicatorService, private val indicatorService: IndicatorService,
private val directLogoutPresenter: Presenter<DirectLogoutState>,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val sessionEnterpriseService: SessionEnterpriseService, private val sessionEnterpriseService: SessionEnterpriseService,
@@ -65,8 +63,6 @@ class PreferencesAccountPresenter(
canDeactivateAccount = matrixClient.canDeactivateAccount() canDeactivateAccount = matrixClient.canDeactivateAccount()
} }
val directLogoutState = directLogoutPresenter.present()
val accountManagementUrl by produceState<String?>(initialValue = null) { val accountManagementUrl by produceState<String?>(initialValue = null) {
value = matrixClient.getAccountManagementUrl(null) value = matrixClient.getAccountManagementUrl(null)
.getOrNull() .getOrNull()
@@ -83,7 +79,6 @@ class PreferencesAccountPresenter(
canReportBug = canReportBug, canReportBug = canReportBug,
showLinkNewDevice = showLinkNewDevice, showLinkNewDevice = showLinkNewDevice,
canDeactivateAccount = canDeactivateAccount, canDeactivateAccount = canDeactivateAccount,
directLogoutState = directLogoutState,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
) )
} }
@@ -7,7 +7,6 @@
package io.element.android.features.preferences.impl.account package io.element.android.features.preferences.impl.account
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -19,6 +18,5 @@ data class PreferencesAccountState(
val canReportBug: Boolean, val canReportBug: Boolean,
val showLinkNewDevice: Boolean, val showLinkNewDevice: Boolean,
val canDeactivateAccount: Boolean, val canDeactivateAccount: Boolean,
val directLogoutState: DirectLogoutState,
val snackbarMessage: SnackbarMessage?, val snackbarMessage: SnackbarMessage?,
) )
@@ -8,8 +8,6 @@
package io.element.android.features.preferences.impl.account package io.element.android.features.preferences.impl.account
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
@@ -56,7 +54,6 @@ fun aPreferencesAccountState(
canReportBug: Boolean = false, canReportBug: Boolean = false,
showLinkNewDevice: Boolean = false, showLinkNewDevice: Boolean = false,
canDeactivateAccount: Boolean = false, canDeactivateAccount: Boolean = false,
directLogoutState: DirectLogoutState = aDirectLogoutState(),
snackbarMessage: SnackbarMessage? = null, snackbarMessage: SnackbarMessage? = null,
) = PreferencesAccountState( ) = PreferencesAccountState(
myUser = myUser, myUser = myUser,
@@ -66,6 +63,5 @@ fun aPreferencesAccountState(
canReportBug = canReportBug, canReportBug = canReportBug,
showLinkNewDevice = showLinkNewDevice, showLinkNewDevice = showLinkNewDevice,
canDeactivateAccount = canDeactivateAccount, canDeactivateAccount = canDeactivateAccount,
directLogoutState = directLogoutState,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
) )
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -38,7 +37,6 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.matrix.ui.components.MatrixUserRow
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BlockedUsersView( fun BlockedUsersView(
state: BlockedUsersState, state: BlockedUsersState,
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -40,7 +39,6 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
* The contents of the Labs screen. * The contents of the Labs screen.
* Design: https://www.figma.com/design/V0dkfRAW6T3yCQKjahpzkX/ER-46-EX--Threads?node-id=2004-27319&t=yssy1yYYigsGON3s-0 * Design: https://www.figma.com/design/V0dkfRAW6T3yCQKjahpzkX/ER-46-EX--Threads?node-id=2004-27319&t=yssy1yYYigsGON3s-0
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LabsView( fun LabsView(
state: LabsState, state: LabsState,
@@ -19,8 +19,6 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.logout.api.direct.DirectLogoutEvent
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.features.preferences.impl.account.PreferencesAccountCallback import io.element.android.features.preferences.impl.account.PreferencesAccountCallback
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
@@ -34,7 +32,6 @@ class PreferencesRootNode(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: PreferencesRootPresenter, private val presenter: PreferencesRootPresenter,
private val emojiPickerRenderer: EmojiPickerRenderer, private val emojiPickerRenderer: EmojiPickerRenderer,
private val directLogoutView: DirectLogoutView,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : PreferencesAccountCallback, Plugin { interface Callback : PreferencesAccountCallback, Plugin {
fun navigateToAddAccount() fun navigateToAddAccount()
@@ -89,16 +86,8 @@ class PreferencesRootNode(
onManageAccountClick = { onManageAccountClick(activity, it, isDark) }, onManageAccountClick = { onManageAccountClick(activity, it, isDark) },
onOpenNotificationSettings = callback::navigateToNotificationSettings, onOpenNotificationSettings = callback::navigateToNotificationSettings,
onEditProfileClick = callback::navigateToUserProfile, onEditProfileClick = callback::navigateToUserProfile,
onSignOutClick = { onSignOutClick = callback::startSignOutFlow,
if (state.preferencesAccountState.directLogoutState.canDoDirectSignOut) {
state.preferencesAccountState.directLogoutState.eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false))
} else {
callback.startSignOutFlow()
}
},
onDeactivateClick = callback::startAccountDeactivationFlow onDeactivateClick = callback::startAccountDeactivationFlow
) )
directLogoutView.Render(state = state.preferencesAccountState.directLogoutState)
} }
} }
@@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -56,7 +55,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
/** /**
* https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=3182-36115&t=U1vS3px9HzlzWYd7-4 * https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=3182-36115&t=U1vS3px9HzlzWYd7-4
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun EditUserProfileView( fun EditUserProfileView(
state: EditUserProfileState, state: EditUserProfileState,
@@ -10,7 +10,6 @@ package io.element.android.features.preferences.impl.account
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
@@ -65,7 +64,6 @@ class PreferencesAccountPresenterTest {
assertThat(initialState.showSecureBackupBadge).isFalse() assertThat(initialState.showSecureBackupBadge).isFalse()
assertThat(initialState.accountManagementUrl).isNull() assertThat(initialState.accountManagementUrl).isNull()
assertThat(initialState.showLinkNewDevice).isFalse() assertThat(initialState.showLinkNewDevice).isFalse()
assertThat(initialState.directLogoutState).isEqualTo(aDirectLogoutState())
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
val loadedState = consumeItemsUntilPredicate { it.accountManagementUrl != null }.last() val loadedState = consumeItemsUntilPredicate { it.accountManagementUrl != null }.last()
assertThat(loadedState.showSecureBackup).isFalse() assertThat(loadedState.showSecureBackup).isFalse()
@@ -169,7 +167,6 @@ class PreferencesAccountPresenterTest {
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),
indicatorService = indicatorService, indicatorService = indicatorService,
directLogoutPresenter = { aDirectLogoutState() },
rageshakeFeatureAvailability = rageshakeFeatureAvailability, rageshakeFeatureAvailability = rageshakeFeatureAvailability,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
sessionEnterpriseService = sessionEnterpriseService, sessionEnterpriseService = sessionEnterpriseService,
@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
@@ -40,7 +39,6 @@ import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ReportRoomView( fun ReportRoomView(
state: ReportRoomState, state: ReportRoomState,
@@ -12,7 +12,6 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -31,7 +30,6 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ChangeRoomPermissionsView( fun ChangeRoomPermissionsView(
state: ChangeRoomPermissionsState, state: ChangeRoomPermissionsState,
@@ -28,7 +28,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -72,7 +71,6 @@ import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ChangeRolesView( fun ChangeRolesView(
state: ChangeRolesState, state: ChangeRolesState,
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -132,7 +131,6 @@ private fun RoomAliasResolverContent(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun RoomAliasResolverTopBar( private fun RoomAliasResolverTopBar(
onBackClick: () -> Unit, onBackClick: () -> Unit,
+1 -1
View File
@@ -7,7 +7,7 @@
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-compose-library")
id("kotlin-parcelize") id("kotlin-parcelize")
} }
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2026 Element Creations 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.features.roomdetails.api
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
/** Optional room-details sections contributed by a build-specific implementation. */
fun interface RoomDetailsExtension {
@Composable
fun ColumnScope.Render(modifier: Modifier)
}
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2026 Element Creations 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.features.roomdetails.impl
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.roomdetails.api.RoomDetailsExtension
import io.element.android.libraries.di.RoomScope
@ContributesBinding(RoomScope::class)
class NoopRoomDetailsExtension : RoomDetailsExtension {
@Composable
override fun ColumnScope.Render(modifier: Modifier) = Unit
}
@@ -24,6 +24,7 @@ import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.leaveroom.api.LeaveRoomRenderer import io.element.android.features.leaveroom.api.LeaveRoomRenderer
import io.element.android.features.roomdetails.api.RoomDetailsExtension
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.architecture.appyx.launchMolecule
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
@@ -46,6 +47,7 @@ class RoomDetailsNode(
private val room: BaseRoom, private val room: BaseRoom,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val leaveRoomRenderer: LeaveRoomRenderer, private val leaveRoomRenderer: LeaveRoomRenderer,
private val roomDetailsExtension: RoomDetailsExtension,
) : Node(buildContext, plugins = plugins), RoomDetailsNavigator { ) : Node(buildContext, plugins = plugins), RoomDetailsNavigator {
interface Callback : Plugin { interface Callback : Plugin {
fun navigateBack() fun navigateBack()
@@ -120,6 +122,7 @@ class RoomDetailsNode(
RoomDetailsView( RoomDetailsView(
state = state, state = state,
additionalSections = { with(roomDetailsExtension) { Render(Modifier) } },
modifier = modifier, modifier = modifier,
goBack = ::navigateUp, goBack = ::navigateUp,
onActionClick = ::onActionClick, onActionClick = ::onActionClick,
@@ -14,6 +14,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
@@ -24,7 +25,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -124,6 +124,7 @@ fun RoomDetailsView(
onProfileClick: (UserId) -> Unit, onProfileClick: (UserId) -> Unit,
onReportRoomClick: () -> Unit, onReportRoomClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
additionalSections: @Composable ColumnScope.() -> Unit = {},
leaveRoomView: @Composable () -> Unit, leaveRoomView: @Composable () -> Unit,
) { ) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@@ -199,6 +200,8 @@ fun RoomDetailsView(
) )
} }
additionalSections()
PreferenceCategory { PreferenceCategory {
if (state.hasNewContent) { if (state.hasNewContent) {
ListItem( ListItem(
@@ -356,7 +359,6 @@ private fun KnockRequestsItem(knockRequestsCount: Int?, onKnockRequestsClick: ()
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun RoomDetailsTopBar( private fun RoomDetailsTopBar(
goBack: () -> Unit, goBack: () -> Unit,
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -76,7 +75,6 @@ fun RoomInviteMembersView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun RoomInviteMembersTopBar( private fun RoomInviteMembersTopBar(
canSend: Boolean, canSend: Boolean,
@@ -27,7 +27,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -352,7 +351,6 @@ private fun RoomMemberListItem(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun RoomMemberListTopBar( private fun RoomMemberListTopBar(
canInvite: Boolean, canInvite: Boolean,
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -160,7 +159,6 @@ private fun RoomSpecificNotificationSettingsView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun RoomNotificationSettingsTopBar( private fun RoomNotificationSettingsTopBar(
onBackClick: () -> Unit, onBackClick: () -> Unit,
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -90,7 +89,6 @@ fun UserDefinedRoomNotificationSettingsView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun UserDefinedRoomNotificationSettingsTopBar( private fun UserDefinedRoomNotificationSettingsTopBar(
roomName: String, roomName: String,
@@ -42,6 +42,9 @@
<string name="screen_room_change_role_unsaved_changes_description">"You have unsaved changes."</string> <string name="screen_room_change_role_unsaved_changes_description">"You have unsaved changes."</string>
<string name="screen_room_change_role_unsaved_changes_title">"Save changes?"</string> <string name="screen_room_change_role_unsaved_changes_title">"Save changes?"</string>
<string name="screen_room_details_add_topic_title">"Add topic"</string> <string name="screen_room_details_add_topic_title">"Add topic"</string>
<string name="screen_room_details_audio_push_to_talk">"Push to talk"</string>
<string name="screen_room_details_audio_title">"Room Audio"</string>
<string name="screen_room_details_audio_video_calls">"Video calls"</string>
<string name="screen_room_details_badge_encrypted">"Encrypted"</string> <string name="screen_room_details_badge_encrypted">"Encrypted"</string>
<string name="screen_room_details_badge_not_encrypted">"Not encrypted"</string> <string name="screen_room_details_badge_not_encrypted">"Not encrypted"</string>
<string name="screen_room_details_badge_public">"Public room"</string> <string name="screen_room_details_badge_public">"Public room"</string>
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.roomdetails.impl
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.tests.testutils.robolectric.RobolectricTest
import org.junit.Test
class NoopRoomDetailsExtensionTest : RobolectricTest() {
@Test
fun `default extension renders no additional content`() = runAndroidComposeUiTest<ComponentActivity> {
val extension = NoopRoomDetailsExtension()
setContent { Column { with(extension) { Render(Modifier) } } }
onRoot().onChildren().assertCountEquals(0)
}
@Test
fun `default extension leaves surrounding content unchanged during updates`() = runAndroidComposeUiTest<ComponentActivity> {
val extension = NoopRoomDetailsExtension()
val text = mutableStateOf("Before")
setContent {
Column {
BasicText(text.value)
with(extension) { Render(Modifier) }
}
}
onNodeWithText("Before").assertExists()
onRoot().onChildren().assertCountEquals(1)
runOnIdle { text.value = "After" }
onNodeWithText("After").assertExists()
onRoot().onChildren().assertCountEquals(1)
}
}
@@ -11,16 +11,20 @@
package io.element.android.features.roomdetails.impl package io.element.android.features.roomdetails.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.AndroidComposeUiTest import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onLast import androidx.compose.ui.test.onLast
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.features.userprofile.shared.aUserProfileState import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.designsystem.theme.components.Text
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.notification.CallIntent import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@@ -41,6 +45,12 @@ import org.junit.Test
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
class RoomDetailsViewTest : RobolectricTest() { class RoomDetailsViewTest : RobolectricTest() {
@Test
fun `extension section is rendered`() = runAndroidComposeUiTest {
setRoomDetailView(additionalSections = { Text("Extra section") })
onNodeWithText("Extra section").assertExists()
}
@Test @Test
fun `click on back invokes expected callback`() = runAndroidComposeUiTest { fun `click on back invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
@@ -381,6 +391,7 @@ private fun AndroidComposeUiTest<ComponentActivity>.setRoomDetailView(
onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(), onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(),
onProfileClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), onProfileClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
onReportRoomClick: () -> Unit = EnsureNeverCalled(), onReportRoomClick: () -> Unit = EnsureNeverCalled(),
additionalSections: @Composable ColumnScope.() -> Unit = {},
) { ) {
setContent { setContent {
RoomDetailsView( RoomDetailsView(
@@ -402,6 +413,7 @@ private fun AndroidComposeUiTest<ComponentActivity>.setRoomDetailView(
onProfileClick = onProfileClick, onProfileClick = onProfileClick,
onReportRoomClick = onReportRoomClick, onReportRoomClick = onReportRoomClick,
leaveRoomView = {}, leaveRoomView = {},
additionalSections = additionalSections,
) )
} }
} }
@@ -6,8 +6,6 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.roomdetailsedit.impl package io.element.android.features.roomdetailsedit.impl
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
@@ -19,7 +17,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -83,7 +82,6 @@ fun RoomDirectoryView(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun RoomDirectoryTopBar( private fun RoomDirectoryTopBar(
onBackClick: () -> Unit, onBackClick: () -> Unit,
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -87,7 +86,6 @@ fun EditRoomAddressView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun EditRoomAddressTopBar( private fun EditRoomAddressTopBar(
isSaveActionEnabled: Boolean, isSaveActionEnabled: Boolean,
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -166,7 +165,6 @@ private fun CheckableSpaceListItem(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ManageAuthorizedSpacesTopBar( private fun ManageAuthorizedSpacesTopBar(
isDoneButtonEnabled: Boolean, isDoneButtonEnabled: Boolean,
@@ -22,7 +22,6 @@ import androidx.compose.foundation.progressSemantics
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -156,7 +155,6 @@ fun SecurityAndPrivacyView(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun SecurityAndPrivacyToolbar( private fun SecurityAndPrivacyToolbar(
isSaveActionEnabled: Boolean, isSaveActionEnabled: Boolean,
+7 -1
View File
@@ -16,6 +16,12 @@ plugins {
android { android {
namespace = "io.element.android.features.signedout.impl" namespace = "io.element.android.features.signedout.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
} }
setupDependencyInjection() setupDependencyInjection()
@@ -30,7 +36,7 @@ dependencies {
implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
testCommonDependencies(libs) testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.sessionStorage.test)
} }
@@ -9,5 +9,5 @@
package io.element.android.features.signedout.impl package io.element.android.features.signedout.impl
sealed interface SignedOutEvent { sealed interface SignedOutEvent {
data object SignInAgain : SignedOutEvent data object Submit : SignedOutEvent
} }
@@ -17,17 +17,18 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
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.user.MatrixUser
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@AssistedInject @AssistedInject
class SignedOutPresenter( class SignedOutPresenter(
@Assisted private val sessionId: SessionId, @Assisted private val sessionId: SessionId,
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
private val buildMeta: BuildMeta,
) : Presenter<SignedOutState> { ) : Presenter<SignedOutState> {
@AssistedFactory @AssistedFactory
fun interface Factory { fun interface Factory {
@@ -37,24 +38,31 @@ class SignedOutPresenter(
@Composable @Composable
override fun present(): SignedOutState { override fun present(): SignedOutState {
val signedOutSession by remember { val signedOutSession by remember {
sessionStore.sessionsFlow().map { sessions -> sessionStore.sessionsFlow()
sessions.firstOrNull { it.userId == sessionId.value } // Ignore the removal of the session, to keep rendering the account data until the screen is closed
} .mapNotNull { sessions -> sessions.firstOrNull { it.userId == sessionId.value } }
}.collectAsState(initial = null) .map { it.toMatrixUser() }
}.collectAsState(initial = MatrixUser(userId = sessionId))
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
fun handleEvent(event: SignedOutEvent) { fun handleEvent(event: SignedOutEvent) {
when (event) { when (event) {
SignedOutEvent.SignInAgain -> coroutineScope.launch { SignedOutEvent.Submit -> coroutineScope.launch {
sessionStore.removeSession(sessionId.value) sessionStore.removeSession(sessionId.value)
} }
} }
} }
return SignedOutState( return SignedOutState(
appName = buildMeta.applicationName, signedOutMatrixUser = signedOutSession,
signedOutSession = signedOutSession,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
private fun SessionData.toMatrixUser() = MatrixUser(
userId = sessionId,
displayName = userDisplayName,
avatarUrl = userAvatarUrl,
avatarThumbnail = userAvatarData,
)
} }
@@ -8,10 +8,9 @@
package io.element.android.features.signedout.impl package io.element.android.features.signedout.impl
import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.matrix.api.user.MatrixUser
data class SignedOutState( data class SignedOutState(
val appName: String, val signedOutMatrixUser: MatrixUser,
val signedOutSession: SessionData?,
val eventSink: (SignedOutEvent) -> Unit, val eventSink: (SignedOutEvent) -> Unit,
) )
@@ -9,44 +9,21 @@
package io.element.android.features.signedout.impl package io.element.android.features.signedout.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.matrix.ui.components.aMatrixUser
open class SignedOutStatePreviewParam : PreviewParameterProvider<SignedOutState> { open class SignedOutStatePreviewParam : PreviewParameterProvider<SignedOutState> {
override val values: Sequence<SignedOutState> override val values: Sequence<SignedOutState>
get() = sequenceOf( get() = sequenceOf(
aSignedOutState(), aSignedOutState(),
// Add other states here aSignedOutState(signedOutSession = aMatrixUser(id = "@alice:server.org", displayName = null)),
) )
} }
private fun aSignedOutState() = SignedOutState( internal fun aSignedOutState(
appName = "AppName", signedOutSession: MatrixUser = aMatrixUser(id = "@alice:server.org", displayName = "Alice"),
signedOutSession = aSessionData(), eventSink: (SignedOutEvent) -> Unit = {},
eventSink = {}, ) = SignedOutState(
signedOutMatrixUser = signedOutSession,
eventSink = eventSink,
) )
private fun aSessionData(
sessionId: String = "@alice:server.org",
isTokenValid: Boolean = false,
): SessionData {
return SessionData(
userId = sessionId,
deviceId = "aDeviceId",
accessToken = "anAccessToken",
refreshToken = "aRefreshToken",
homeserverUrl = "aHomeserverUrl",
oAuthData = null,
loginTimestamp = null,
isTokenValid = isTokenValid,
loginType = LoginType.UNKNOWN,
passphrase = null,
sessionPath = "/a/path/to/a/session",
cachePath = "/a/path/to/a/cache",
position = 0,
lastUsageIndex = 0,
userDisplayName = null,
userAvatarUrl = null,
userAvatarData = null,
)
}
@@ -9,14 +9,14 @@
package io.element.android.features.signedout.impl package io.element.android.features.signedout.impl
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -31,58 +31,75 @@ import io.element.android.libraries.designsystem.components.visuallist.VisualLis
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCard
import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCardMode
import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCardState
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
/**
* Ref: https://www.figma.com/design/kEAcfun9iSpszeUDvdKZ6b/ER-351--Multi-account-in-EX?node-id=856-6104
*/
@Composable @Composable
fun SignedOutView( fun SignedOutView(
state: SignedOutState, state: SignedOutState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
BackHandler(onBack = { state.eventSink(SignedOutEvent.SignInAgain) }) BackHandler(onBack = { state.eventSink(SignedOutEvent.Submit) })
HeaderFooterPage( HeaderFooterPage(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.systemBarsPadding() .systemBarsPadding()
.imePadding(), .imePadding(),
header = { SignedOutHeader(state) }, isScrollable = true,
content = { SignedOutContent() }, header = { SignedOutHeader() },
content = { SignedOutContent(state.signedOutMatrixUser) },
footer = { footer = {
SignedOutFooter( SignedOutFooter(
onSignInAgain = { state.eventSink(SignedOutEvent.SignInAgain) }, onSubmit = { state.eventSink(SignedOutEvent.Submit) },
) )
} }
) )
} }
@Composable @Composable
private fun SignedOutHeader(state: SignedOutState) { private fun SignedOutHeader() {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), modifier = Modifier.padding(top = 60.dp, bottom = 12.dp),
title = stringResource(id = R.string.screen_signed_out_title), title = stringResource(id = R.string.screen_signed_out_title),
subTitle = stringResource(id = R.string.screen_signed_out_subtitle, state.appName), subTitle = stringResource(id = R.string.screen_signed_out_subtitle_v2),
iconStyle = BigIcon.Style.Default(CompoundIcons.UserProfileSolid()), iconStyle = BigIcon.Style.Default(
vectorIcon = CompoundIcons.Devices(),
),
) )
} }
@Composable @Composable
private fun SignedOutContent() { private fun SignedOutContent(
Box( signedOutMatrixUser: MatrixUser,
modifier = Modifier.fillMaxSize(), ) {
contentAlignment = BiasAlignment( Column(
horizontalBias = 0f, modifier = Modifier
verticalBias = -0.4f .fillMaxSize()
) .padding(top = 8.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) { ) {
AccountInfoCard(
AccountInfoCardState(
matrixUser = signedOutMatrixUser,
mode = AccountInfoCardMode.Simple,
)
)
VisualList( VisualList(
items = persistentListOf( items = persistentListOf(
VisualListItemData( VisualListItemData(
message = stringResource(id = R.string.screen_signed_out_reason_1), message = stringResource(id = R.string.screen_signed_out_reason_1),
iconVector = CompoundIcons.Lock(), iconVector = CompoundIcons.UserProfile(),
), ),
VisualListItemData( VisualListItemData(
message = stringResource(id = R.string.screen_signed_out_reason_2), message = stringResource(id = R.string.screen_signed_out_reason_2),
iconVector = CompoundIcons.Devices(), iconVector = CompoundIcons.Lock(),
), ),
VisualListItemData( VisualListItemData(
message = stringResource(id = R.string.screen_signed_out_reason_3), message = stringResource(id = R.string.screen_signed_out_reason_3),
@@ -95,12 +112,12 @@ private fun SignedOutContent() {
@Composable @Composable
private fun SignedOutFooter( private fun SignedOutFooter(
onSignInAgain: () -> Unit, onSubmit: () -> Unit,
) { ) {
ButtonColumnMolecule { ButtonColumnMolecule {
Button( Button(
text = stringResource(id = CommonStrings.action_sign_in_again), text = stringResource(id = CommonStrings.action_ok),
onClick = onSignInAgain, onClick = onSubmit,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
} }
@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signed_out_reason_1">"You’ve changed your password on another session"</string> <string name="screen_signed_out_reason_1">"You removed this device from your account"</string>
<string name="screen_signed_out_reason_2">"You have deleted the session from another session"</string> <string name="screen_signed_out_reason_2">"You’ve changed your password on another device"</string>
<string name="screen_signed_out_reason_3">"Your server’s administrator has invalidated your access"</string> <string name="screen_signed_out_reason_3">"Your server\'s session expired or the administrator has blocked your access"</string>
<string name="screen_signed_out_subtitle">"You might have been signed out for one of the reasons listed below. Please sign in again to continue using %s."</string> <string name="screen_signed_out_subtitle">"You might have been signed out for one of the reasons listed below. Please sign in again to continue using %s."</string>
<string name="screen_signed_out_title">"You’re signed out"</string> <string name="screen_signed_out_subtitle_v2">"This device was removed for one of the reasons listed below. Please sign in again to connect this device to your account."</string>
<string name="screen_signed_out_title">"Device removed"</string>
</resources> </resources>
@@ -13,13 +13,14 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
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.AN_APPLICATION_NAME import io.element.android.libraries.matrix.api.user.MatrixUser
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.core.aBuildMeta
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
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
@@ -30,7 +31,12 @@ class SignedOutPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val aSessionData = aSessionData() val aSessionData = aSessionData(
sessionId = A_SESSION_ID.value,
userDisplayName = "Alice",
userAvatarUrl = "avatarUrl",
userAvatarData = "avatarData",
)
val sessionStore = InMemorySessionStore( val sessionStore = InMemorySessionStore(
initialList = listOf(aSessionData) initialList = listOf(aSessionData)
) )
@@ -38,16 +44,26 @@ class SignedOutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.appName).isEqualTo(AN_APPLICATION_NAME) assertThat(initialState.signedOutMatrixUser).isEqualTo(MatrixUser(userId = A_SESSION_ID))
assertThat(initialState.signedOutSession).isEqualTo(aSessionData) assertThat(awaitItem().signedOutMatrixUser).isEqualTo(
MatrixUser(
userId = A_SESSION_ID,
displayName = "Alice",
avatarUrl = "avatarUrl",
avatarThumbnail = "avatarData",
)
)
} }
} }
@OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun `present - sign in again`() = runTest { fun `present - sign in again`() = runTest {
val aSessionData = aSessionData() val aSessionData = aSessionData(
sessionId = A_SESSION_ID.value,
userDisplayName = "Alice",
)
val sessionStore = InMemorySessionStore( val sessionStore = InMemorySessionStore(
initialList = listOf(aSessionData) initialList = listOf(aSessionData)
) )
@@ -57,13 +73,15 @@ class SignedOutPresenterTest {
}.test { }.test {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.signedOutSession).isEqualTo(aSessionData) assertThat(initialState.signedOutMatrixUser).isEqualTo(MatrixUser(userId = A_SESSION_ID, displayName = "Alice"))
assertThat(sessionStore.getAllSessions()).isNotEmpty() assertThat(sessionStore.getAllSessions()).isNotEmpty()
assertThat(sessionStore.numberOfSessions()).isEqualTo(1) assertThat(sessionStore.numberOfSessions()).isEqualTo(1)
initialState.eventSink(SignedOutEvent.SignInAgain) initialState.eventSink(SignedOutEvent.Submit)
assertThat(awaitItem().signedOutSession).isNull() runCurrent()
assertThat(sessionStore.getAllSessions()).isEmpty() assertThat(sessionStore.getAllSessions()).isEmpty()
assertThat(sessionStore.numberOfSessions()).isEqualTo(0) assertThat(sessionStore.numberOfSessions()).isEqualTo(0)
// The removal of the session must not change the rendered data
expectNoEvents()
} }
} }
} }
@@ -75,6 +93,5 @@ internal fun createSignedOutPresenter(
return SignedOutPresenter( return SignedOutPresenter(
sessionId = sessionId, sessionId = sessionId,
sessionStore = sessionStore, sessionStore = sessionStore,
buildMeta = aBuildMeta(applicationName = AN_APPLICATION_NAME),
) )
} }

Some files were not shown because too many files have changed in this diff Show More