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:
id: "home_screen-settings"
- tapOn:
id: "settings-user_profile"
- assertVisible: "Your account"
- tapOn: "Remove this device"
- takeScreenshot: build/maestro/900-SignOutScreen
- back
- tapOn: "Remove this device"
# Ensure pressing back cancels
- back
- tapOn: "Remove this device"
# Ensure cancel cancels
- tapOn: "Cancel"
- tapOn: "Remove this device"
- tapOn:
text: "Remove this device"
index: 1
# Cancel the dialog cancels the logout
- tapOn:
id: "dialog-negative"
- tapOn: "Remove this device"
- tapOn:
text: "Remove this device"
index: 1
- tapOn:
id: "dialog-positive"
- runFlow: ../assertions/assertInitDisplayed.yaml
+1 -1
View File
@@ -1,7 +1,7 @@
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.
@@ -6,12 +6,9 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.appnav.room.joined
import android.os.Parcelable
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -68,7 +67,6 @@ fun LoadingRoomNodeView(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LoadingRoomTopBar(
onBackClick: () -> Unit,
@@ -121,7 +121,6 @@ private fun FullscreenAnnouncementHeader(
subTitle = announcement.subtitle(),
iconStyle = BigIcon.Style.Default(
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -68,7 +67,6 @@ fun AddPeopleView(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AddPeopleTopBar(
onSkipClick: () -> Unit,
@@ -24,7 +24,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -179,7 +178,6 @@ fun ConfigureRoomView(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ConfigureRoomToolbar(
isSpace: Boolean,
@@ -23,7 +23,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
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 kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccountDeactivationView(
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.EnterpriseService
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.uri.ensureProtocol
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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import timber.log.Timber
@@ -31,15 +33,16 @@ import timber.log.Timber
class DefaultEnterpriseService(
private val temporaryMatrixClientFactory: TemporaryMatrixClientFactory,
private val jsonProvider: JsonProvider,
private val dispatchers: CoroutineDispatchers,
) : EnterpriseService {
override suspend fun isEnterpriseUser(sessionId: SessionId) = false
override suspend fun tweakMasUrl(url: String, urlContentFetcher: ClientUrlContentFetcher) = url
override fun accountProviderAllowList(): List<AccountProvider> = emptyList()
override fun canConnectToAnyAccountProvider(): Boolean = true
override suspend fun isAllowedToConnectToAccountProvider(accountProvider: AccountProvider) = true
override suspend fun isElementProEnforced(serverName: String): Boolean {
val temporaryMatrixClient = temporaryMatrixClientFactory.create(serverName).getOrElse { return false }
return temporaryMatrixClient.use { client ->
override suspend fun isElementProEnforced(serverName: String): Boolean = withContext(dispatchers.io) {
val temporaryMatrixClient = temporaryMatrixClientFactory.create(serverName).getOrElse { return@withContext false }
temporaryMatrixClient.use { client ->
val baseUrl = serverName.ensureProtocol().removeSuffix("/")
// 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")
@@ -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.tests.testutils.lambda.lambdaRecorder
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.serialization.json.Json
import org.junit.Test
class DefaultEnterpriseServiceTest {
@Test
fun accountProviderAllowList() {
fun accountProviderAllowList() = runTest {
val defaultEnterpriseService = createDefaultEnterpriseService()
assertThat(defaultEnterpriseService.accountProviderAllowList()).isEmpty()
}
@Test
fun `canConnectToAnyAccountProvider is always true`() {
fun `canConnectToAnyAccountProvider is always true`() = runTest {
val defaultEnterpriseService = createDefaultEnterpriseService()
assertThat(defaultEnterpriseService.canConnectToAnyAccountProvider()).isTrue()
}
@@ -132,11 +134,12 @@ class DefaultEnterpriseServiceTest {
closeLambda.assertions().isCalledOnce()
}
private fun createDefaultEnterpriseService(
private fun TestScope.createDefaultEnterpriseService(
client: FakeTemporaryMatrixClient = FakeTemporaryMatrixClient(),
jsonProvider: JsonProvider = { Json }
) = DefaultEnterpriseService(
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(createResult = { Result.success(client) }),
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChooseSelfVerificationModeView(
state: ChooseSelfVerificationModeState,
-2
View File
@@ -53,7 +53,6 @@ dependencies {
implementation(projects.features.announcement.api)
implementation(projects.features.invite.api)
implementation(projects.features.networkmonitor.api)
implementation(projects.features.logout.api)
implementation(projects.features.leaveroom.api)
implementation(projects.features.rageshake.api)
implementation(projects.services.analytics.api)
@@ -67,7 +66,6 @@ dependencies {
testCommonDependencies(libs, true)
testImplementation(projects.features.announcement.test)
testImplementation(projects.features.invite.test)
testImplementation(projects.features.logout.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.libraries.matrix.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.declineandblock.DeclineInviteAndBlockEntryPoint
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.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
@@ -83,7 +82,6 @@ class HomeFlowNode(
private val inviteFriendsUseCase: InviteFriendsUseCase,
private val analyticsService: AnalyticsService,
private val acceptDeclineInviteView: AcceptDeclineInviteView,
private val directLogoutView: DirectLogoutView,
private val reportRoomEntryPoint: ReportRoomEntryPoint,
private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint,
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 io.element.android.features.home.impl.roomlist.RoomListState
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.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -42,7 +41,6 @@ class HomePresenter(
private val indicatorService: IndicatorService,
private val roomListPresenter: Presenter<RoomListState>,
private val homeSpacesPresenter: Presenter<HomeSpacesState>,
private val logoutPresenter: Presenter<DirectLogoutState>,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val sessionStore: SessionStore,
) : Presenter<HomeState> {
@@ -75,7 +73,6 @@ class HomePresenter(
}
// Avatar indicator
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
val directLogoutState = logoutPresenter.present()
fun handleEvent(event: HomeEvent) {
when (event) {
@@ -98,7 +95,6 @@ class HomePresenter(
homeSpacesState = homeSpacesState,
snackbarMessage = snackbarMessage,
canReportBug = canReportBug,
directLogoutState = directLogoutState,
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.spacefilters.SpaceFiltersState
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.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
@@ -29,7 +28,6 @@ data class HomeState(
val homeSpacesState: HomeSpacesState,
val snackbarMessage: SnackbarMessage?,
val canReportBug: Boolean,
val directLogoutState: DirectLogoutState,
val eventSink: (HomeEvent) -> Unit,
) {
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.spaces.HomeSpacesState
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.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -63,7 +61,6 @@ internal fun aHomeState(
roomListState: RoomListState = aRoomListState(),
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
canReportBug: Boolean = true,
directLogoutState: DirectLogoutState = aDirectLogoutState(),
eventSink: (HomeEvent) -> Unit = {}
) = HomeState(
currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
@@ -71,7 +68,6 @@ internal fun aHomeState(
hasNetworkConnection = hasNetworkConnection,
snackbarMessage = snackbarMessage,
canReportBug = canReportBug,
directLogoutState = directLogoutState,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
roomListState = roomListState,
homeSpacesState = homeSpacesState,
@@ -22,8 +22,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.plus
import androidx.compose.foundation.layout.windowInsetsPadding
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.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
@@ -150,7 +148,6 @@ fun HomeView(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun HomeScaffold(
state: HomeState,
@@ -187,7 +184,7 @@ private fun HomeScaffold(
val spacesLazyListState = rememberLazyListState()
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
modifier = modifier,
topBar = {
HomeTopBar(
selectedNavigationItem = state.currentHomeNavigationBarItem,
@@ -268,42 +265,50 @@ private fun HomeScaffold(
val contentPadding = PaddingValues(
bottom = 96.dp,
)
when (state.currentHomeNavigationBarItem) {
HomeNavigationBarItem.Chats -> {
RoomListContentView(
contentState = roomListState.contentState,
filtersState = roomListState.filtersState,
spaceFiltersState = roomListState.spaceFiltersState,
lazyListState = roomsLazyListState,
hideInvitesAvatars = roomListState.hideInvitesAvatars,
eventSink = roomListState.eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = ::onRoomClick,
onCreateRoomClick = onStartChatClick,
contentPadding = lazyColumnContentPadding + contentPadding,
modifier = Modifier
.padding(outerPadding)
.consumeWindowInsets(outerPadding)
)
SpaceFiltersView(roomListState.spaceFiltersState)
}
HomeNavigationBarItem.Spaces -> {
HomeSpacesView(
modifier = Modifier
.fillMaxSize()
.padding(outerPadding)
.consumeWindowInsets(outerPadding),
contentPadding = lazyColumnContentPadding + contentPadding,
state = state.homeSpacesState,
lazyListState = spacesLazyListState,
onSpaceClick = { spaceId ->
onRoomClick(spaceId)
},
onCreateSpaceClick = onCreateSpaceClick,
// TODO use actual callbacks for this
onExploreClick = {},
)
// The nested scroll connection is set on the content and not on the Scaffold, else the top bar would
// consume the scroll events of its own content.
Box(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection)
) {
when (state.currentHomeNavigationBarItem) {
HomeNavigationBarItem.Chats -> {
RoomListContentView(
contentState = roomListState.contentState,
filtersState = roomListState.filtersState,
spaceFiltersState = roomListState.spaceFiltersState,
lazyListState = roomsLazyListState,
hideInvitesAvatars = roomListState.hideInvitesAvatars,
eventSink = roomListState.eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = ::onRoomClick,
onCreateRoomClick = onStartChatClick,
contentPadding = lazyColumnContentPadding + contentPadding,
modifier = Modifier
.padding(outerPadding)
.consumeWindowInsets(outerPadding)
)
SpaceFiltersView(roomListState.spaceFiltersState)
}
HomeNavigationBarItem.Spaces -> {
HomeSpacesView(
modifier = Modifier
.fillMaxSize()
.padding(outerPadding)
.consumeWindowInsets(outerPadding),
contentPadding = lazyColumnContentPadding + contentPadding,
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
private fun HomeBottomBar(
currentHomeNavigationBarItem: HomeNavigationBarItem,
@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
@@ -100,7 +99,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeTopBar(
selectedNavigationItem: HomeNavigationBarItem,
@@ -449,7 +447,6 @@ private fun StatusEmojiBadge(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
internal fun HomeTopBarPreview() = ElementPreview {
@@ -470,7 +467,6 @@ internal fun HomeTopBarPreview() = ElementPreview {
)
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview {
@@ -491,7 +487,6 @@ internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview {
)
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
internal fun HomeTopBarSpacesPreview() = ElementPreview {
@@ -512,7 +507,6 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview {
)
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
@@ -533,7 +527,6 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
)
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
internal fun HomeTopBarWithStatusPreview() = ElementPreview {
@@ -560,7 +553,6 @@ internal fun HomeTopBarWithStatusPreview() = ElementPreview {
)
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
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.rememberLazyListState
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
@@ -88,7 +87,6 @@ internal fun RoomListSearchView(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomListSearchContent(
state: RoomListSearchState,
@@ -36,7 +36,6 @@ class DefaultHomeEntryPointTest : RobolectricTest() {
inviteFriendsUseCase = { lambdaError() },
analyticsService = FakeAnalyticsService(),
acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
directLogoutView = { _ -> lambdaError() },
reportRoomEntryPoint = { _, _, _ -> lambdaError() },
declineInviteAndBlockUserEntryPoint = FakeDeclineInviteAndBlockEntryPoint(),
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.spaces.HomeSpacesState
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.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -162,7 +161,6 @@ internal fun createHomePresenter(
indicatorService = indicatorService,
roomListPresenter = { aRoomListState() },
homeSpacesPresenter = homeSpacesPresenter,
logoutPresenter = { aDirectLogoutState() },
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
sessionStore = sessionStore,
)
@@ -217,6 +217,10 @@ class RoomListPresenterTest {
val presenter = createRoomListPresenter(client = client)
presenter.test {
val initialState = awaitItem()
// Skip intermediate event for loading the value of the `UnreadIndicatorCount` feature flag
skipItems(1)
val summary = createRoomListRoomSummary()
initialState.eventSink(RoomListEvent.ShowContextMenu(summary))
@@ -260,6 +264,10 @@ class RoomListPresenterTest {
val presenter = createRoomListPresenter(client = client)
presenter.test {
val initialState = awaitItem()
// Skip intermediate event for loading the value of the `UnreadIndicatorCount` feature flag
skipItems(1)
val summary = createRoomListRoomSummary()
initialState.eventSink(RoomListEvent.ShowContextMenu(summary))
@@ -309,6 +317,10 @@ class RoomListPresenterTest {
)
presenter.test {
val initialState = awaitItem()
// Skip intermediate event for loading the value of the `UnreadIndicatorCount` feature flag
skipItems(1)
eventRecorder.assertEmpty()
initialState.eventSink(RoomListEvent.ToggleSearchResults)
eventRecorder.assertSingle(
@@ -398,6 +410,7 @@ class RoomListPresenterTest {
client = matrixClient,
)
presenter.test {
skipItems(1) // Skip initial state
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java)
}
}
@@ -616,7 +629,7 @@ class RoomListPresenterTest {
)
presenter.test {
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()
assertThat(state.contentAsRooms().showNewNotificationSoundBanner).isFalse()
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DeclineAndBlockView(
state: DeclineAndBlockState,
@@ -171,7 +171,6 @@ private fun InvitePeopleContentView(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun InvitePeopleSearchBar(
queryState: TextFieldState,
@@ -24,7 +24,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -593,7 +592,6 @@ private fun DefaultLoadedContent(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun JoinRoomTopBar(
contentState: ContentState,
@@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -479,7 +478,6 @@ private fun KnockRequestsEmptyList(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun KnockRequestsListTopBar(onBackClick: () -> Unit) {
TopAppBar(
@@ -12,7 +12,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.plus
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.scaffoldScrollableContentInsets
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DependenciesDetailsView(
licenseItem: DependencyLicenseItem,
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.plus
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
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.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DependencyLicensesListView(
state: DependencyLicensesListState,
@@ -5,8 +5,6 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.linknewdevice.impl.screens.desktop
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.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -5,8 +5,6 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.linknewdevice.impl.screens.number
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.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -5,8 +5,6 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.linknewdevice.impl.screens.qrcode
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.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -5,12 +5,9 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.linknewdevice.impl.screens.root
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -6,8 +6,6 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.lockscreen.impl.setup.pin
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
@@ -12,7 +12,10 @@ package io.element.android.features.lockscreen.impl.unlock.keypad
import android.view.KeyEvent
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.platform.LocalInputModeManager
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.hasContentDescription
@@ -52,7 +55,9 @@ class PinKeypadTest : RobolectricTest() {
@Test
fun `typing using the hardware keyboard emits the expected events`() = runAndroidComposeUiTest {
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()
onAllNodes(isRoot())[0].performKeyInput {
val keys = listOf(
@@ -115,13 +120,16 @@ class PinKeypadTest : RobolectricTest() {
private fun AndroidComposeUiTest<ComponentActivity>.setPinKeyPad(
onClick: (PinKeypadModel) -> Unit = EnsureNeverCalledWithParam(),
) {
): InputModeManager {
lateinit var inputModeManager: InputModeManager
setContent {
inputModeManager = LocalInputModeManager.current
PinKeypad(
onClick = onClick,
maxWidth = 1000.dp,
maxHeight = 1000.dp,
)
}
return inputModeManager
}
}
@@ -25,6 +25,7 @@ import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.features.login.impl.BuildConfig
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.uri.ensureProtocol
import io.element.android.libraries.di.annotations.AppCoroutineScope
@@ -69,6 +70,7 @@ class DefaultElementClassicConnection(
private val coroutineScope: CoroutineScope,
private val matrixAuthenticationService: MatrixAuthenticationService,
private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker,
private val dispatchers: CoroutineDispatchers,
) : ElementClassicConnection {
// Messenger for communicating with the service.
private var messenger: Messenger? = null
@@ -221,7 +223,7 @@ class DefaultElementClassicConnection(
fun onSessionReceived(data: Bundle) {
// The data must be extracted from the bundle before we launch the coroutine, else the bundle will be emptied
val state = data.toElementClassicConnectionState()
coroutineScope.launch {
coroutineScope.launch(dispatchers.io) {
val updatedState = ensureHomeserverIsSupported(state)
emitState(updatedState)
val userId = (updatedState as? ElementClassicConnectionState.ElementClassicReady)?.elementClassicSession?.userId
@@ -6,8 +6,6 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.login.impl.screens.chooseaccountprovider
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
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.systemBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
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.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginWithClassicView(
state: LoginWithClassicState,
@@ -23,7 +23,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
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.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginPasswordView(
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.value
import io.element.android.tests.testutils.robolectric.RobolectricTest
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -499,5 +500,6 @@ class DefaultElementClassicConnectionTest : RobolectricTest() {
coroutineScope = coroutineScope,
matrixAuthenticationService = matrixAuthenticationService,
homeServerLoginCompatibilityChecker = homeServerLoginCompatibilityChecker,
dispatchers = testCoroutineDispatchers(),
)
}
@@ -11,7 +11,6 @@ package io.element.android.features.logout.api.direct
import io.element.android.libraries.architecture.AsyncAction
data class DirectLogoutState(
val canDoDirectSignOut: Boolean,
val logoutAction: AsyncAction<Unit>,
val eventSink: (DirectLogoutEvent) -> Unit,
)
@@ -23,11 +23,9 @@ open class DirectLogoutStatePreviewParam : PreviewParameterProvider<DirectLogout
}
fun aDirectLogoutState(
canDoDirectSignOut: Boolean = true,
logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (DirectLogoutEvent) -> Unit = {},
) = DirectLogoutState(
canDoDirectSignOut = canDoDirectSignOut,
logoutAction = logoutAction,
eventSink = eventSink,
)
+1
View File
@@ -31,6 +31,7 @@ dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
@@ -41,6 +41,7 @@ class LogoutPresenter(
@Composable
override fun present(): LogoutState {
val localCoroutineScope = rememberCoroutineScope()
val currentUser by matrixClient.userProfile.collectAsState()
val logoutAction: MutableState<AsyncAction<Unit>> = remember {
mutableStateOf(AsyncAction.Uninitialized)
}
@@ -90,6 +91,7 @@ class LogoutPresenter(
}
return LogoutState(
currentUser = currentUser,
isLastDevice = isLastDevice,
backupState = backupState,
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.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.user.MatrixUser
data class LogoutState(
val currentUser: MatrixUser,
val isLastDevice: Boolean,
val backupState: BackupState,
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.RecoveryState
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> {
override val values: Sequence<LogoutState>
@@ -39,10 +41,12 @@ open class LogoutStatePreviewParam : PreviewParameterProvider<LogoutState> {
backupUploadState = BackupUploadState.Waiting,
waitingForALongTime = true,
),
aLogoutState(currentUser = aMatrixUser(id = "@alice:server.org", displayName = null)),
)
}
fun aLogoutState(
currentUser: MatrixUser = aMatrixUser(),
isLastDevice: Boolean = false,
backupState: BackupState = BackupState.ENABLED,
doesBackupExistOnServer: Boolean = true,
@@ -52,6 +56,7 @@ fun aLogoutState(
logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (LogoutEvent) -> Unit = {},
) = LogoutState(
currentUser = currentUser,
isLastDevice = isLastDevice,
backupState = backupState,
doesBackupExistOnServer = doesBackupExistOnServer,
@@ -11,7 +11,9 @@ package io.element.android.features.logout.impl
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
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.OutlinedButton
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.matrix.api.encryption.BackupState
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.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.testTag
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
fun LogoutView(
state: LogoutState,
@@ -54,15 +63,17 @@ fun LogoutView(
onBackClick = onBackClick,
title = title(state),
subTitle = subtitle(state),
iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()),
iconStyle = BigIcon.Style.Default(CompoundIcons.Mobile()),
modifier = modifier,
isScrollable = true,
buttons = {
Buttons(
state = state,
onChangeRecoveryKeyClick = onChangeRecoveryKeyClick,
onLogoutClick = {
eventSink(LogoutEvent.Logout(ignoreSdkError = false))
}
},
onCancelClick = onBackClick,
)
},
) {
@@ -71,6 +82,8 @@ fun LogoutView(
LogoutActionDialog(
state.logoutAction,
// Do not show the progress dialog, the progress is rendered in the button.
showProgressDialog = false,
onConfirmClick = {
eventSink(LogoutEvent.Logout(ignoreSdkError = false))
},
@@ -116,6 +129,7 @@ private fun ColumnScope.Buttons(
state: LogoutState,
onLogoutClick: () -> Unit,
onChangeRecoveryKeyClick: () -> Unit,
onCancelClick: () -> Unit,
) {
val logoutAction = state.logoutAction
if (state.isLastDevice) {
@@ -139,6 +153,12 @@ private fun ColumnScope.Buttons(
.testTag(TestTags.signOut),
onClick = onLogoutClick,
)
TextButton(
text = stringResource(id = CommonStrings.action_cancel),
enabled = logoutAction !is AsyncAction.Loading,
modifier = Modifier.fillMaxWidth(),
onClick = onCancelClick,
)
}
@Composable
@@ -149,9 +169,16 @@ private fun Content(
Column(
modifier = modifier
.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)
) {
AccountInfoCard(
AccountInfoCardState(
matrixUser = state.currentUser,
mode = AccountInfoCardMode.Simple,
)
)
Spacer(modifier = Modifier.height(16.dp))
when (state.backupUploadState) {
is BackupUploadState.Uploading -> {
LinearProgressIndicator(
@@ -27,6 +27,7 @@ class DefaultDirectLogoutView : DirectLogoutView {
val eventSink = state.eventSink
LogoutActionDialog(
state.logoutAction,
showProgressDialog = true,
onConfirmClick = {
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.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.features.logout.api.direct.DirectLogoutEvent
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.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
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.launch
@Inject
class DirectLogoutPresenter(
private val matrixClient: MatrixClient,
private val encryptionService: EncryptionService,
) : Presenter<DirectLogoutState> {
@Composable
override fun present(): DirectLogoutState {
@@ -41,13 +35,6 @@ class DirectLogoutPresenter(
mutableStateOf(AsyncAction.Uninitialized)
}
val backupUploadState: BackupUploadState by remember {
encryptionService.waitForBackupUploadSteadyState()
}
.collectAsState(initial = BackupUploadState.Unknown)
val isLastDevice by encryptionService.isLastDevice.collectAsState()
fun handleEvent(event: DirectLogoutEvent) {
when (event) {
is DirectLogoutEvent.Logout -> {
@@ -64,8 +51,6 @@ class DirectLogoutPresenter(
}
return DirectLogoutState(
canDoDirectSignOut = !isLastDevice &&
!backupUploadState.isBackingUp(),
logoutAction = logoutAction.value,
eventSink = ::handleEvent,
)
@@ -19,6 +19,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun LogoutActionDialog(
state: AsyncAction<Unit>,
showProgressDialog: Boolean,
onConfirmClick: () -> Unit,
onForceLogoutClick: () -> Unit,
onDismissDialog: () -> Unit,
@@ -31,8 +32,11 @@ fun LogoutActionDialog(
onSubmitClick = onConfirmClick,
onDismiss = onDismissDialog
)
is AsyncAction.Loading ->
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
is AsyncAction.Loading -> {
if (showProgressDialog) {
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
}
}
is AsyncAction.Failure ->
RetryDialog(
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.EncryptionService
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.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
@@ -39,11 +41,18 @@ class LogoutPresenterTest {
@Test
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) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.currentUser).isEqualTo(MatrixUser(A_SESSION_ID, "Alice", "mxc://avatar"))
assertThat(initialState.isLastDevice).isFalse()
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
assertThat(initialState.doesBackupExistOnServer).isTrue()
@@ -13,8 +13,11 @@ package io.element.android.features.logout.impl
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.AndroidComposeUiTest
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 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.ui.strings.CommonStrings
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.robolectric.RobolectricTest
import org.junit.Test
import org.robolectric.annotation.Config
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
fun `clicking on logout sends a LogoutEvent`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LogoutEvent>()
@@ -88,10 +105,24 @@ class LogoutViewTest : RobolectricTest() {
eventSink = eventsRecorder
),
)
clickOn(CommonStrings.action_cancel)
clickOn(CommonStrings.action_cancel, inDialog = true)
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
fun `last session setting button invoke onChangeRecoveryKeyClicked`() = runAndroidComposeUiTest {
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.libraries.architecture.AsyncAction
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.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -38,44 +34,6 @@ class DirectLogoutPresenterTest {
presenter.present()
}.test {
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)
}
}
@@ -180,9 +138,7 @@ class DirectLogoutPresenterTest {
private fun createDirectLogoutPresenter(
matrixClient: MatrixClient = FakeMatrixClient(),
encryptionService: EncryptionService = FakeEncryptionService(),
): DirectLogoutPresenter = DirectLogoutPresenter(
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.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AttachmentsPreviewView(
state: AttachmentsPreviewState,
@@ -263,7 +261,7 @@ private fun AttachmentSendStateView(
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun AttachmentPreviewContent(
state: AttachmentsPreviewState,
@@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AttachmentImageEditorView(
state: AttachmentImageEditorState,
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.plus
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -107,7 +106,6 @@ fun PinnedMessagesListView(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PinnedMessagesListTopBar(
state: PinnedMessagesListState,
@@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
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.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReportMessageView(
state: ReportMessageState,
@@ -23,7 +23,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@@ -74,7 +73,6 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ThreadsListView(
state: ThreadsListState,
@@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -57,7 +56,6 @@ import org.json.JSONObject
* Screen used to display debug info for events.
* It will only be available in debug builds.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EventDebugInfoView(
eventId: EventId?,
@@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -61,7 +60,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun MessagesViewTopBar(
roomName: String?,
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -43,7 +42,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ThreadTopBar(
roomName: String?,
@@ -22,7 +22,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
@@ -216,7 +215,6 @@ fun CreatePollView(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CreatePollTopAppBar(
mode: CreatePollState.Mode,
@@ -23,7 +23,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.runtime.Composable
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 kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PollHistoryView(
state: PollHistoryState,
@@ -125,7 +123,6 @@ fun PollHistoryView(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PollHistoryFilterButtons(
activeFilter: PollHistoryFilter,
@@ -17,7 +17,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
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.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -34,7 +33,6 @@ class PreferencesAccountPresenter(
private val sessionVerificationService: SessionVerificationService,
private val snackbarDispatcher: SnackbarDispatcher,
private val indicatorService: IndicatorService,
private val directLogoutPresenter: Presenter<DirectLogoutState>,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val featureFlagService: FeatureFlagService,
private val sessionEnterpriseService: SessionEnterpriseService,
@@ -65,8 +63,6 @@ class PreferencesAccountPresenter(
canDeactivateAccount = matrixClient.canDeactivateAccount()
}
val directLogoutState = directLogoutPresenter.present()
val accountManagementUrl by produceState<String?>(initialValue = null) {
value = matrixClient.getAccountManagementUrl(null)
.getOrNull()
@@ -83,7 +79,6 @@ class PreferencesAccountPresenter(
canReportBug = canReportBug,
showLinkNewDevice = showLinkNewDevice,
canDeactivateAccount = canDeactivateAccount,
directLogoutState = directLogoutState,
snackbarMessage = snackbarMessage,
)
}
@@ -7,7 +7,6 @@
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.matrix.api.user.MatrixUser
@@ -19,6 +18,5 @@ data class PreferencesAccountState(
val canReportBug: Boolean,
val showLinkNewDevice: Boolean,
val canDeactivateAccount: Boolean,
val directLogoutState: DirectLogoutState,
val snackbarMessage: SnackbarMessage?,
)
@@ -8,8 +8,6 @@
package io.element.android.features.preferences.impl.account
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.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUser
@@ -56,7 +54,6 @@ fun aPreferencesAccountState(
canReportBug: Boolean = false,
showLinkNewDevice: Boolean = false,
canDeactivateAccount: Boolean = false,
directLogoutState: DirectLogoutState = aDirectLogoutState(),
snackbarMessage: SnackbarMessage? = null,
) = PreferencesAccountState(
myUser = myUser,
@@ -66,6 +63,5 @@ fun aPreferencesAccountState(
canReportBug = canReportBug,
showLinkNewDevice = showLinkNewDevice,
canDeactivateAccount = canDeactivateAccount,
directLogoutState = directLogoutState,
snackbarMessage = snackbarMessage,
)
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BlockedUsersView(
state: BlockedUsersState,
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.
* Design: https://www.figma.com/design/V0dkfRAW6T3yCQKjahpzkX/ER-46-EX--Threads?node-id=2004-27319&t=yssy1yYYigsGON3s-0
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LabsView(
state: LabsState,
@@ -19,8 +19,6 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
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.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.callback
@@ -34,7 +32,6 @@ class PreferencesRootNode(
@Assisted plugins: List<Plugin>,
private val presenter: PreferencesRootPresenter,
private val emojiPickerRenderer: EmojiPickerRenderer,
private val directLogoutView: DirectLogoutView,
) : Node(buildContext, plugins = plugins) {
interface Callback : PreferencesAccountCallback, Plugin {
fun navigateToAddAccount()
@@ -89,16 +86,8 @@ class PreferencesRootNode(
onManageAccountClick = { onManageAccountClick(activity, it, isDark) },
onOpenNotificationSettings = callback::navigateToNotificationSettings,
onEditProfileClick = callback::navigateToUserProfile,
onSignOutClick = {
if (state.preferencesAccountState.directLogoutState.canDoDirectSignOut) {
state.preferencesAccountState.directLogoutState.eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false))
} else {
callback.startSignOutFlow()
}
},
onSignOutClick = callback::startSignOutFlow,
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
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
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditUserProfileView(
state: EditUserProfileState,
@@ -10,7 +10,6 @@ package io.element.android.features.preferences.impl.account
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.api.SessionEnterpriseService
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.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlagService
@@ -65,7 +64,6 @@ class PreferencesAccountPresenterTest {
assertThat(initialState.showSecureBackupBadge).isFalse()
assertThat(initialState.accountManagementUrl).isNull()
assertThat(initialState.showLinkNewDevice).isFalse()
assertThat(initialState.directLogoutState).isEqualTo(aDirectLogoutState())
assertThat(initialState.snackbarMessage).isNull()
val loadedState = consumeItemsUntilPredicate { it.accountManagementUrl != null }.last()
assertThat(loadedState.showSecureBackup).isFalse()
@@ -169,7 +167,6 @@ class PreferencesAccountPresenterTest {
sessionVerificationService = sessionVerificationService,
snackbarDispatcher = SnackbarDispatcher(),
indicatorService = indicatorService,
directLogoutPresenter = { aDirectLogoutState() },
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
featureFlagService = featureFlagService,
sessionEnterpriseService = sessionEnterpriseService,
@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReportRoomView(
state: ReportRoomState,
@@ -12,7 +12,6 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChangeRoomPermissionsView(
state: ChangeRoomPermissionsState,
@@ -28,7 +28,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
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 kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChangeRolesView(
state: ChangeRolesState,
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -132,7 +131,6 @@ private fun RoomAliasResolverContent(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomAliasResolverTopBar(
onBackClick: () -> Unit,
+1 -1
View File
@@ -7,7 +7,7 @@
*/
plugins {
id("io.element.android-library")
id("io.element.android-compose-library")
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 io.element.android.annotations.ContributesNode
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.architecture.appyx.launchMolecule
import io.element.android.libraries.architecture.callback
@@ -46,6 +47,7 @@ class RoomDetailsNode(
private val room: BaseRoom,
private val analyticsService: AnalyticsService,
private val leaveRoomRenderer: LeaveRoomRenderer,
private val roomDetailsExtension: RoomDetailsExtension,
) : Node(buildContext, plugins = plugins), RoomDetailsNavigator {
interface Callback : Plugin {
fun navigateBack()
@@ -120,6 +122,7 @@ class RoomDetailsNode(
RoomDetailsView(
state = state,
additionalSections = { with(roomDetailsExtension) { Render(Modifier) } },
modifier = modifier,
goBack = ::navigateUp,
onActionClick = ::onActionClick,
@@ -14,6 +14,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -124,6 +124,7 @@ fun RoomDetailsView(
onProfileClick: (UserId) -> Unit,
onReportRoomClick: () -> Unit,
modifier: Modifier = Modifier,
additionalSections: @Composable ColumnScope.() -> Unit = {},
leaveRoomView: @Composable () -> Unit,
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@@ -199,6 +200,8 @@ fun RoomDetailsView(
)
}
additionalSections()
PreferenceCategory {
if (state.hasNewContent) {
ListItem(
@@ -356,7 +359,6 @@ private fun KnockRequestsItem(knockRequestsCount: Int?, onKnockRequestsClick: ()
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomDetailsTopBar(
goBack: () -> Unit,
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -76,7 +75,6 @@ fun RoomInviteMembersView(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomInviteMembersTopBar(
canSend: Boolean,
@@ -27,7 +27,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -352,7 +351,6 @@ private fun RoomMemberListItem(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomMemberListTopBar(
canInvite: Boolean,
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -160,7 +159,6 @@ private fun RoomSpecificNotificationSettingsView(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomNotificationSettingsTopBar(
onBackClick: () -> Unit,
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -90,7 +89,6 @@ fun UserDefinedRoomNotificationSettingsView(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun UserDefinedRoomNotificationSettingsTopBar(
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_title">"Save changes?"</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_not_encrypted">"Not encrypted"</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
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.ExperimentalTestApi
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onLast
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.roomdetails.impl.members.aRoomMember
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.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@@ -41,6 +45,12 @@ import org.junit.Test
import org.robolectric.annotation.Config
class RoomDetailsViewTest : RobolectricTest() {
@Test
fun `extension section is rendered`() = runAndroidComposeUiTest {
setRoomDetailView(additionalSections = { Text("Extra section") })
onNodeWithText("Extra section").assertExists()
}
@Test
fun `click on back invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback ->
@@ -381,6 +391,7 @@ private fun AndroidComposeUiTest<ComponentActivity>.setRoomDetailView(
onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(),
onProfileClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
onReportRoomClick: () -> Unit = EnsureNeverCalled(),
additionalSections: @Composable ColumnScope.() -> Unit = {},
) {
setContent {
RoomDetailsView(
@@ -402,6 +413,7 @@ private fun AndroidComposeUiTest<ComponentActivity>.setRoomDetailView(
onProfileClick = onProfileClick,
onReportRoomClick = onReportRoomClick,
leaveRoomView = {},
additionalSections = additionalSections,
)
}
}
@@ -6,8 +6,6 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.roomdetailsedit.impl
import androidx.activity.compose.BackHandler
@@ -19,7 +17,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
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.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
@@ -83,7 +82,6 @@ fun RoomDirectoryView(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomDirectoryTopBar(
onBackClick: () -> Unit,
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -87,7 +86,6 @@ fun EditRoomAddressView(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun EditRoomAddressTopBar(
isSaveActionEnabled: Boolean,
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -166,7 +165,6 @@ private fun CheckableSpaceListItem(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ManageAuthorizedSpacesTopBar(
isDoneButtonEnabled: Boolean,
@@ -22,7 +22,6 @@ import androidx.compose.foundation.progressSemantics
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItemDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -156,7 +155,6 @@ fun SecurityAndPrivacyView(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SecurityAndPrivacyToolbar(
isSaveActionEnabled: Boolean,
+7 -1
View File
@@ -16,6 +16,12 @@ plugins {
android {
namespace = "io.element.android.features.signedout.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
setupDependencyInjection()
@@ -30,7 +36,7 @@ dependencies {
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.uiStrings)
testCommonDependencies(libs)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.sessionStorage.test)
}
@@ -9,5 +9,5 @@
package io.element.android.features.signedout.impl
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.AssistedInject
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.user.MatrixUser
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
@AssistedInject
class SignedOutPresenter(
@Assisted private val sessionId: SessionId,
private val sessionStore: SessionStore,
private val buildMeta: BuildMeta,
) : Presenter<SignedOutState> {
@AssistedFactory
fun interface Factory {
@@ -37,24 +38,31 @@ class SignedOutPresenter(
@Composable
override fun present(): SignedOutState {
val signedOutSession by remember {
sessionStore.sessionsFlow().map { sessions ->
sessions.firstOrNull { it.userId == sessionId.value }
}
}.collectAsState(initial = null)
sessionStore.sessionsFlow()
// 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 } }
.map { it.toMatrixUser() }
}.collectAsState(initial = MatrixUser(userId = sessionId))
val coroutineScope = rememberCoroutineScope()
fun handleEvent(event: SignedOutEvent) {
when (event) {
SignedOutEvent.SignInAgain -> coroutineScope.launch {
SignedOutEvent.Submit -> coroutineScope.launch {
sessionStore.removeSession(sessionId.value)
}
}
}
return SignedOutState(
appName = buildMeta.applicationName,
signedOutSession = signedOutSession,
signedOutMatrixUser = signedOutSession,
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
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.matrix.api.user.MatrixUser
data class SignedOutState(
val appName: String,
val signedOutSession: SessionData?,
val signedOutMatrixUser: MatrixUser,
val eventSink: (SignedOutEvent) -> Unit,
)
@@ -9,44 +9,21 @@
package io.element.android.features.signedout.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUser
open class SignedOutStatePreviewParam : PreviewParameterProvider<SignedOutState> {
override val values: Sequence<SignedOutState>
get() = sequenceOf(
aSignedOutState(),
// Add other states here
aSignedOutState(signedOutSession = aMatrixUser(id = "@alice:server.org", displayName = null)),
)
}
private fun aSignedOutState() = SignedOutState(
appName = "AppName",
signedOutSession = aSessionData(),
eventSink = {},
internal fun aSignedOutState(
signedOutSession: MatrixUser = aMatrixUser(id = "@alice:server.org", displayName = "Alice"),
eventSink: (SignedOutEvent) -> Unit = {},
) = 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
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.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.PreviewsDayNight
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 kotlinx.collections.immutable.persistentListOf
/**
* Ref: https://www.figma.com/design/kEAcfun9iSpszeUDvdKZ6b/ER-351--Multi-account-in-EX?node-id=856-6104
*/
@Composable
fun SignedOutView(
state: SignedOutState,
modifier: Modifier = Modifier,
) {
BackHandler(onBack = { state.eventSink(SignedOutEvent.SignInAgain) })
BackHandler(onBack = { state.eventSink(SignedOutEvent.Submit) })
HeaderFooterPage(
modifier = modifier
.fillMaxSize()
.systemBarsPadding()
.imePadding(),
header = { SignedOutHeader(state) },
content = { SignedOutContent() },
isScrollable = true,
header = { SignedOutHeader() },
content = { SignedOutContent(state.signedOutMatrixUser) },
footer = {
SignedOutFooter(
onSignInAgain = { state.eventSink(SignedOutEvent.SignInAgain) },
onSubmit = { state.eventSink(SignedOutEvent.Submit) },
)
}
)
}
@Composable
private fun SignedOutHeader(state: SignedOutState) {
private fun SignedOutHeader() {
IconTitleSubtitleMolecule(
modifier = Modifier.padding(top = 60.dp, bottom = 12.dp),
title = stringResource(id = R.string.screen_signed_out_title),
subTitle = stringResource(id = R.string.screen_signed_out_subtitle, state.appName),
iconStyle = BigIcon.Style.Default(CompoundIcons.UserProfileSolid()),
subTitle = stringResource(id = R.string.screen_signed_out_subtitle_v2),
iconStyle = BigIcon.Style.Default(
vectorIcon = CompoundIcons.Devices(),
),
)
}
@Composable
private fun SignedOutContent() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = BiasAlignment(
horizontalBias = 0f,
verticalBias = -0.4f
)
private fun SignedOutContent(
signedOutMatrixUser: MatrixUser,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(top = 8.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
AccountInfoCard(
AccountInfoCardState(
matrixUser = signedOutMatrixUser,
mode = AccountInfoCardMode.Simple,
)
)
VisualList(
items = persistentListOf(
VisualListItemData(
message = stringResource(id = R.string.screen_signed_out_reason_1),
iconVector = CompoundIcons.Lock(),
iconVector = CompoundIcons.UserProfile(),
),
VisualListItemData(
message = stringResource(id = R.string.screen_signed_out_reason_2),
iconVector = CompoundIcons.Devices(),
iconVector = CompoundIcons.Lock(),
),
VisualListItemData(
message = stringResource(id = R.string.screen_signed_out_reason_3),
@@ -95,12 +112,12 @@ private fun SignedOutContent() {
@Composable
private fun SignedOutFooter(
onSignInAgain: () -> Unit,
onSubmit: () -> Unit,
) {
ButtonColumnMolecule {
Button(
text = stringResource(id = CommonStrings.action_sign_in_again),
onClick = onSignInAgain,
text = stringResource(id = CommonStrings.action_ok),
onClick = onSubmit,
modifier = Modifier.fillMaxWidth(),
)
}
@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<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_2">"You have deleted the session from another session"</string>
<string name="screen_signed_out_reason_3">"Your server’s administrator has invalidated your access"</string>
<string name="screen_signed_out_reason_1">"You removed this device from your account"</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 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_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>
@@ -13,13 +13,14 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
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.core.aBuildMeta
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -30,7 +31,12 @@ class SignedOutPresenterTest {
@Test
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(
initialList = listOf(aSessionData)
)
@@ -38,16 +44,26 @@ class SignedOutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.appName).isEqualTo(AN_APPLICATION_NAME)
assertThat(initialState.signedOutSession).isEqualTo(aSessionData)
assertThat(initialState.signedOutMatrixUser).isEqualTo(MatrixUser(userId = A_SESSION_ID))
assertThat(awaitItem().signedOutMatrixUser).isEqualTo(
MatrixUser(
userId = A_SESSION_ID,
displayName = "Alice",
avatarUrl = "avatarUrl",
avatarThumbnail = "avatarData",
)
)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - sign in again`() = runTest {
val aSessionData = aSessionData()
val aSessionData = aSessionData(
sessionId = A_SESSION_ID.value,
userDisplayName = "Alice",
)
val sessionStore = InMemorySessionStore(
initialList = listOf(aSessionData)
)
@@ -57,13 +73,15 @@ class SignedOutPresenterTest {
}.test {
skipItems(1)
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.numberOfSessions()).isEqualTo(1)
initialState.eventSink(SignedOutEvent.SignInAgain)
assertThat(awaitItem().signedOutSession).isNull()
initialState.eventSink(SignedOutEvent.Submit)
runCurrent()
assertThat(sessionStore.getAllSessions()).isEmpty()
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(
sessionId = sessionId,
sessionStore = sessionStore,
buildMeta = aBuildMeta(applicationName = AN_APPLICATION_NAME),
)
}

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