Merge branch 'develop' into feature/fga/space_list_join_action

This commit is contained in:
ganfra 2025-09-29 18:01:42 +02:00
commit 0390fde615
591 changed files with 6155 additions and 2140 deletions

View file

@ -16,14 +16,14 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class AnalyticsOptInNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -19,7 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.call.api.CallType
@ -49,7 +49,7 @@ import timber.log.Timber
import java.util.UUID
import kotlin.time.Duration.Companion.seconds
@Inject
@AssistedInject
class CallScreenPresenter(
@Assisted private val callType: CallType,
@Assisted private val navigator: CallScreenNavigator,

View file

@ -133,6 +133,7 @@ class WebViewWidgetMessageInterceptor(
return assetLoader.shouldInterceptRequest(request.url)
}
@Suppress("OVERRIDE_DEPRECATION")
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
return assetLoader.shouldInterceptRequest(url.toUri())
}

View file

@ -15,7 +15,7 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
import io.element.android.libraries.architecture.NodeInputs
@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import kotlinx.coroutines.flow.first
@ContributesNode(RoomScope::class)
@Inject
@AssistedInject
class ChangeRolesNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -20,7 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.RoomModeration
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
@ -47,7 +47,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@Inject
@AssistedInject
class ChangeRolesPresenter(
@Assisted private val role: RoomMember.Role,
private val room: JoinedRoom,

View file

@ -17,7 +17,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.di.RoomComponentFactory
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
@ -32,7 +32,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class ChangeRoomMemberRolesRootNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -17,7 +17,7 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.replace
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class CreateRoomFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -14,7 +14,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
import io.element.android.features.invitepeople.api.InvitePeopleRenderer
@ -24,7 +24,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class AddPeopleNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -15,7 +15,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class ConfigureRoomNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -13,12 +13,12 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class AccountDeactivationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -8,7 +8,7 @@
<string name="screen_deactivate_account_list_item_1">"%1$s a fiókját (nem fog tudni újra bejelentkezni, és az azonosítója nem használható újra)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Véglegesen letiltja"</string>
<string name="screen_deactivate_account_list_item_2">"Eltávolításra kerül az összes csevegőszobából."</string>
<string name="screen_deactivate_account_list_item_3">"Törlésre kerülnek a fiókadatai a személyazonosító kiszolgálónkról."</string>
<string name="screen_deactivate_account_list_item_3">"Törlésre kerülnek a fiókadatai az azonosítási kiszolgálónkról."</string>
<string name="screen_deactivate_account_list_item_4">"Üzenetei továbbra is láthatóak maradnak a regisztrált felhasználók számára, de nem lesznek elérhetőek az új vagy nem regisztrált felhasználók számára, ha úgy dönt, hogy törli őket."</string>
<string name="screen_deactivate_account_title">"Fiók deaktiválása"</string>
</resources>

View file

@ -22,6 +22,7 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.replace
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.Inject
import io.element.android.annotations.ContributesNode
import io.element.android.features.analytics.api.AnalyticsEntryPoint
@ -42,7 +43,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class FtueFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class NotificationsOptInNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -14,7 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.permissions.api.PermissionStateProvider
@ -25,7 +25,7 @@ import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Inject
@AssistedInject
class NotificationsOptInPresenter(
permissionsPresenterFactory: PermissionsPresenter.Factory,
@Assisted private val callback: NotificationsOptInNode.Callback,

View file

@ -21,7 +21,7 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.appconfig.LearnMoreConfig
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeNode
@ -37,7 +37,7 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class FtueSessionVerificationFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -14,14 +14,14 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class ChooseSelfVerificationModeNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -71,6 +71,7 @@ dependencies {
testImplementation(projects.libraries.permissions.noop)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test)

View file

@ -0,0 +1,69 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.sessionstorage.api.SessionData
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
class CurrentUserWithNeighborsBuilder {
/**
* Build a list of [MatrixUser] containing the current user. If there are other sessions, the list
* will contain 3 users, with the current user in the middle.
* If there is only one other session, the list will contain twice the other user, to allow cycling.
*/
fun build(
matrixUser: MatrixUser,
sessions: List<SessionData>,
): ImmutableList<MatrixUser> {
// Sort by position to always have the same order (not depending on last account usage)
return sessions.sortedBy { it.position }
.map {
if (it.userId == matrixUser.userId.value) {
// Always use the freshest profile for the current user
matrixUser
} else {
// Use the data from the DB
MatrixUser(
userId = UserId(it.userId),
displayName = it.userDisplayName,
avatarUrl = it.userAvatarUrl,
)
}
}
.let { sessionList ->
// If the list has one item, there is no other session, return the list
when (sessionList.size) {
// Can happen when the user signs out (?)
0 -> listOf(matrixUser)
1 -> sessionList
else -> {
// Create a list with extra item at the start and end if necessary to have the current user in the middle
// If the list is [A, B, C, D] and the current user is A we want to return [D, A, B]
// If the current user is B, we want to return [A, B, C]
// If the current user is C, we want to return [B, C, D]
// If the current user is D, we want to return [C, D, A]
// Special case: if there are only two users, we want to return [B, A, B] or [A, B, A] to allows cycling
// between the two users.
val currentUserIndex = sessionList.indexOfFirst { it.userId == matrixUser.userId }
when (currentUserIndex) {
// This can happen when the user signs out.
// In this case, just return a singleton list with the current user.
-1 -> listOf(matrixUser)
0 -> listOf(sessionList.last()) + sessionList.take(2)
sessionList.lastIndex -> sessionList.takeLast(2) + sessionList.first()
else -> sessionList.slice(currentUserIndex - 1..currentUserIndex + 1)
}
}
}
}
.toPersistentList()
}
}

View file

@ -7,6 +7,9 @@
package io.element.android.features.home.impl
import io.element.android.libraries.matrix.api.core.SessionId
sealed interface HomeEvents {
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvents
data class SwitchToAccount(val sessionId: SessionId) : HomeEvents
}

View file

@ -26,7 +26,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
@ -56,7 +56,7 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class HomeFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -14,6 +14,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
@ -29,6 +30,10 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@Inject
class HomePresenter(
@ -41,10 +46,21 @@ class HomePresenter(
private val logoutPresenter: Presenter<DirectLogoutState>,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val featureFlagService: FeatureFlagService,
private val sessionStore: SessionStore,
) : Presenter<HomeState> {
private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
@Composable
override fun present(): HomeState {
val matrixUser = client.userProfile.collectAsState()
val coroutineState = rememberCoroutineScope()
val matrixUser by client.userProfile.collectAsState()
val currentUserAndNeighbors by remember {
combine(
client.userProfile,
sessionStore.sessionsFlow(),
currentUserWithNeighborsBuilder::build,
)
}.collectAsState(initial = persistentListOf(matrixUser))
val isOnline by syncService.isOnline.collectAsState()
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
val roomListState = roomListPresenter.present()
@ -71,6 +87,9 @@ class HomePresenter(
is HomeEvents.SelectHomeNavigationBarItem -> {
currentHomeNavigationBarItemOrdinal = event.item.ordinal
}
is HomeEvents.SwitchToAccount -> coroutineState.launch {
sessionStore.setLatestSession(event.sessionId.value)
}
}
}
@ -82,7 +101,7 @@ class HomePresenter(
}
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
return HomeState(
matrixUser = matrixUser.value,
currentUserAndNeighbors = currentUserAndNeighbors,
showAvatarIndicator = showAvatarIndicator,
hasNetworkConnection = isOnline,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,

View file

@ -13,10 +13,15 @@ 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
@Immutable
data class HomeState(
val matrixUser: MatrixUser,
/**
* The current user of this session, in case of multiple accounts, will contains 3 items, with the
* current user in the middle.
*/
val currentUserAndNeighbors: ImmutableList<MatrixUser>,
val showAvatarIndicator: Boolean,
val hasNetworkConnection: Boolean,
val currentHomeNavigationBarItem: HomeNavigationBarItem,

View file

@ -21,6 +21,7 @@ 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
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toPersistentList
open class HomeStateProvider : PreviewParameterProvider<HomeState> {
override val values: Sequence<HomeState>
@ -50,6 +51,7 @@ open class HomeStateProvider : PreviewParameterProvider<HomeState> {
internal fun aHomeState(
matrixUser: MatrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
currentUserAndNeighbors: List<MatrixUser> = listOf(matrixUser),
showAvatarIndicator: Boolean = false,
hasNetworkConnection: Boolean = true,
snackbarMessage: SnackbarMessage? = null,
@ -61,7 +63,7 @@ internal fun aHomeState(
directLogoutState: DirectLogoutState = aDirectLogoutState(),
eventSink: (HomeEvents) -> Unit = {}
) = HomeState(
matrixUser = matrixUser,
currentUserAndNeighbors = currentUserAndNeighbors.toPersistentList(),
showAvatarIndicator = showAvatarIndicator,
hasNetworkConnection = hasNetworkConnection,
snackbarMessage = snackbarMessage,

View file

@ -171,12 +171,15 @@ private fun HomeScaffold(
topBar = {
RoomListTopBar(
title = stringResource(state.currentHomeNavigationBarItem.labelRes),
matrixUser = state.matrixUser,
currentUserAndNeighbors = state.currentUserAndNeighbors,
showAvatarIndicator = state.showAvatarIndicator,
areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
onToggleSearch = { roomListState.eventSink(RoomListEvents.ToggleSearchResults) },
onMenuActionClick = onMenuActionClick,
onOpenSettings = onOpenSettings,
onAccountSwitch = {
state.eventSink(HomeEvents.SwitchToAccount(it))
},
scrollBehavior = scrollBehavior,
displayMenuItems = state.displayActions,
displayFilters = roomListState.displayFilters && state.currentHomeNavigationBarItem == HomeNavigationBarItem.Chats,

View file

@ -11,19 +11,25 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@ -41,7 +47,6 @@ import io.element.android.features.home.impl.filters.RoomListFiltersView
import io.element.android.features.home.impl.filters.aRoomListFiltersState
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.modifiers.backgroundVerticalGradient
@ -57,23 +62,29 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RoomListTopBar(
title: String,
matrixUser: MatrixUser,
currentUserAndNeighbors: ImmutableList<MatrixUser>,
showAvatarIndicator: Boolean,
areSearchResultsDisplayed: Boolean,
onToggleSearch: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
onOpenSettings: () -> Unit,
onAccountSwitch: (SessionId) -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
displayMenuItems: Boolean,
displayFilters: Boolean,
@ -83,10 +94,11 @@ fun RoomListTopBar(
) {
DefaultRoomListTopBar(
title = title,
matrixUser = matrixUser,
currentUserAndNeighbors = currentUserAndNeighbors,
showAvatarIndicator = showAvatarIndicator,
areSearchResultsDisplayed = areSearchResultsDisplayed,
onOpenSettings = onOpenSettings,
onAccountSwitch = onAccountSwitch,
onSearchClick = onToggleSearch,
onMenuActionClick = onMenuActionClick,
scrollBehavior = scrollBehavior,
@ -102,11 +114,12 @@ fun RoomListTopBar(
@Composable
private fun DefaultRoomListTopBar(
title: String,
matrixUser: MatrixUser,
currentUserAndNeighbors: ImmutableList<MatrixUser>,
showAvatarIndicator: Boolean,
areSearchResultsDisplayed: Boolean,
scrollBehavior: TopAppBarScrollBehavior,
onOpenSettings: () -> Unit,
onAccountSwitch: (SessionId) -> Unit,
onSearchClick: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
displayMenuItems: Boolean,
@ -116,12 +129,6 @@ private fun DefaultRoomListTopBar(
modifier: Modifier = Modifier,
) {
val collapsedFraction = scrollBehavior.state.collapsedFraction
val avatarData by remember(matrixUser) {
derivedStateOf {
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
}
}
Box(modifier = modifier) {
val collapsedTitleTextStyle = ElementTheme.typography.aliasScreenTitle
val expandedTitleTextStyle = ElementTheme.typography.fontHeadingLgBold.copy(
@ -158,8 +165,9 @@ private fun DefaultRoomListTopBar(
},
navigationIcon = {
NavigationIcon(
avatarData = avatarData,
currentUserAndNeighbors = currentUserAndNeighbors,
showAvatarIndicator = showAvatarIndicator,
onAccountSwitch = onAccountSwitch,
onClick = onOpenSettings,
)
},
@ -247,19 +255,67 @@ private fun DefaultRoomListTopBar(
@Composable
private fun NavigationIcon(
avatarData: AvatarData,
currentUserAndNeighbors: ImmutableList<MatrixUser>,
showAvatarIndicator: Boolean,
onAccountSwitch: (SessionId) -> Unit,
onClick: () -> Unit,
) {
if (currentUserAndNeighbors.size == 1) {
AccountIcon(
matrixUser = currentUserAndNeighbors.single(),
isCurrentAccount = true,
showAvatarIndicator = showAvatarIndicator,
onClick = onClick,
)
} else {
// Render a vertical pager
val pagerState = rememberPagerState(initialPage = 1) { currentUserAndNeighbors.size }
// Listen to page changes and switch account if needed
val latestOnAccountSwitch by rememberUpdatedState(onAccountSwitch)
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.settledPage }.collect { page ->
latestOnAccountSwitch(SessionId(currentUserAndNeighbors[page].userId.value))
}
}
VerticalPager(
state = pagerState,
modifier = Modifier.height(48.dp),
) { page ->
AccountIcon(
matrixUser = currentUserAndNeighbors[page],
isCurrentAccount = page == 1,
showAvatarIndicator = page == 1 && showAvatarIndicator,
onClick = if (page == 1) {
onClick
} else {
{}
},
)
}
}
}
@Composable
private fun AccountIcon(
matrixUser: MatrixUser,
isCurrentAccount: Boolean,
showAvatarIndicator: Boolean,
onClick: () -> Unit,
) {
IconButton(
modifier = Modifier.testTag(TestTags.homeScreenSettings),
modifier = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier,
onClick = onClick,
) {
Box {
val avatarData by remember(matrixUser) {
derivedStateOf {
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
}
}
Avatar(
avatarData = avatarData,
avatarType = AvatarType.User,
contentDescription = stringResource(CommonStrings.common_settings),
contentDescription = if (isCurrentAccount) stringResource(CommonStrings.common_settings) else null,
)
if (showAvatarIndicator) {
RedIndicatorAtom(
@ -276,11 +332,12 @@ private fun NavigationIcon(
internal fun DefaultRoomListTopBarPreview() = ElementPreview {
DefaultRoomListTopBar(
title = stringResource(R.string.screen_roomlist_main_space_title),
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = false,
areSearchResultsDisplayed = false,
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {},
onAccountSwitch = {},
onSearchClick = {},
displayMenuItems = true,
displayFilters = true,
@ -296,11 +353,33 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview {
internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview {
DefaultRoomListTopBar(
title = stringResource(R.string.screen_roomlist_main_space_title),
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = true,
areSearchResultsDisplayed = false,
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {},
onAccountSwitch = {},
onSearchClick = {},
displayMenuItems = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
canReportBug = true,
onMenuActionClick = {},
)
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
internal fun DefaultRoomListTopBarMultiAccountPreview() = ElementPreview {
DefaultRoomListTopBar(
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = aMatrixUserList().take(3).toPersistentList(),
showAvatarIndicator = false,
areSearchResultsDisplayed = false,
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {},
onAccountSwitch = {},
onSearchClick = {},
displayMenuItems = true,
displayFilters = true,

View file

@ -6,8 +6,12 @@
<string name="screen_home_tab_chats">"Всички чатове"</string>
<string name="screen_invites_decline_chat_message">"Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Отказване на покана"</string>
<string name="screen_invites_decline_direct_chat_message">"Сигурни ли сте, че искате да откажете този личен чат с %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Отказване на чат"</string>
<string name="screen_invites_empty_list">"Няма покани"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) ви покани"</string>
<string name="screen_migration_message">"Това е еднократен процес, благодаря, че изчакахте."</string>
<string name="screen_migration_title">"Настройване на вашия акаунт."</string>
<string name="screen_roomlist_a11y_create_message">"Създаване на нов разговор или стая"</string>
<string name="screen_roomlist_empty_message">"Започнете, като изпратите съобщение на някого."</string>
<string name="screen_roomlist_empty_title">"Все още няма чатове."</string>

View file

@ -7,5 +7,5 @@
<string name="confirm_recovery_key_banner_primary_button_title">"Enter your backup password"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Forgot your backup password?"</string>
<string name="confirm_recovery_key_banner_title">"Your message backup is out of sync"</string>
<string name="session_verification_banner_message">"Looks like you\'re using a new device. Confirm it with another connected device to access your encrypted messages."</string>
<string name="session_verification_banner_message">"Looks like you\'re using a new device. Confirm it with another linked device to access your encrypted messages."</string>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Kapcsolja ki az alkalmazás akkumulátor-optimalizálását, hogy biztosan megkapja az összes értesítést."</string>
<string name="banner_battery_optimization_content_android">"Kapcsolja ki az alkalmazás akkumulátoroptimalizálását, hogy biztosan megkapja az összes értesítést."</string>
<string name="banner_battery_optimization_submit_android">"Optimalizálás letiltása"</string>
<string name="banner_battery_optimization_title_android">"Nem érkeznek meg az értesítések?"</string>
<string name="banner_set_up_recovery_content">"Hozzon létre egy új helyreállítási kulcsot, amellyel visszaállíthatja a titkosított üzenetek előzményeit, ha elveszíti az eszközökhöz való hozzáférést."</string>

View file

@ -0,0 +1,222 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID_3
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.test.aSessionData
import org.junit.Test
class CurrentUserWithNeighborsBuilderTest {
@Test
fun `build on empty list returns current user`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser()
val list = listOf<SessionData>()
val result = sut.build(matrixUser, list)
assertThat(result).containsExactly(matrixUser)
}
@Test
fun `ensure that account are sorted by position`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
position = 3,
),
aSessionData(
sessionId = A_USER_ID_2.value,
position = 2,
),
aSessionData(
sessionId = A_USER_ID_3.value,
position = 1,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID_3,
A_USER_ID_2,
A_USER_ID,
)
}
@Test
fun `if current user is not found, return a singleton with current user`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID_2.value,
),
aSessionData(
sessionId = A_USER_ID_3.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID,
)
}
@Test
fun `one account, will return a singleton`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID,
)
}
@Test
fun `two accounts, first is current, will return 3 items`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
),
aSessionData(
sessionId = A_USER_ID_2.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID_2,
A_USER_ID,
A_USER_ID_2,
)
}
@Test
fun `two accounts, second is current, will return 3 items`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID_2.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
),
aSessionData(
sessionId = A_USER_ID_2.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID,
A_USER_ID_2,
A_USER_ID,
)
}
@Test
fun `three accounts, first is current, will return last current and next`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
),
aSessionData(
sessionId = A_USER_ID_2.value,
),
aSessionData(
sessionId = A_USER_ID_3.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID_3,
A_USER_ID,
A_USER_ID_2,
)
}
@Test
fun `three accounts, second is current, will return first current and last`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID_2.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
),
aSessionData(
sessionId = A_USER_ID_2.value,
),
aSessionData(
sessionId = A_USER_ID_3.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID,
A_USER_ID_2,
A_USER_ID_3,
)
}
@Test
fun `three accounts, current is last, will return middle, current and first`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID_3.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID_2.value,
),
aSessionData(
sessionId = A_USER_ID_3.value,
),
aSessionData(
sessionId = A_USER_ID.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID,
A_USER_ID_2,
A_USER_ID_3,
)
}
@Test
fun `one account, will return data from matrix user and not from db`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(
id = A_USER_ID.value,
displayName = "Bob",
avatarUrl = "avatarUrl",
)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
userDisplayName = "Outdated Bob",
userAvatarUrl = "outdatedAvatarUrl",
),
)
val result = sut.build(matrixUser, list)
assertThat(result).containsExactly(
MatrixUser(
userId = A_USER_ID,
displayName = "Bob",
avatarUrl = "avatarUrl",
)
)
}
}

View file

@ -32,6 +32,9 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.sync.FakeSyncService
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.MutablePresenter
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
@ -54,17 +57,29 @@ class HomePresenterTest {
val presenter = createHomePresenter(
client = matrixClient,
rageshakeFeatureAvailability = { flowOf(false) },
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(
sessionId = matrixClient.sessionId.value,
userDisplayName = null,
userAvatarUrl = null,
)
),
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.matrixUser).isEqualTo(MatrixUser(A_USER_ID))
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(
MatrixUser(A_USER_ID, null, null)
)
assertThat(initialState.canReportBug).isFalse()
skipItems(1)
val withUserState = awaitItem()
assertThat(withUserState.matrixUser.userId).isEqualTo(A_USER_ID)
assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME)
assertThat(withUserState.matrixUser.avatarUrl).isEqualTo(AN_AVATAR_URL)
assertThat(withUserState.currentUserAndNeighbors.first()).isEqualTo(
MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL)
)
assertThat(withUserState.showAvatarIndicator).isFalse()
assertThat(withUserState.isSpaceFeatureEnabled).isFalse()
assertThat(withUserState.showNavigationBar).isFalse()
@ -75,6 +90,9 @@ class HomePresenterTest {
fun `present - can report bug`() = runTest {
val presenter = createHomePresenter(
rageshakeFeatureAvailability = { flowOf(true) },
sessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -92,6 +110,9 @@ class HomePresenterTest {
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.Space.key to true),
),
sessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
)
presenter.test {
skipItems(1)
@ -105,6 +126,9 @@ class HomePresenterTest {
val indicatorService = FakeIndicatorService()
val presenter = createHomePresenter(
indicatorService = indicatorService,
sessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -124,19 +148,28 @@ class HomePresenterTest {
userAvatarUrl = null,
)
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.failure(AN_EXCEPTION))
val presenter = createHomePresenter(client = matrixClient)
val presenter = createHomePresenter(
client = matrixClient,
sessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.matrixUser).isEqualTo(MatrixUser(matrixClient.sessionId))
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(MatrixUser(matrixClient.sessionId))
// No new state is coming
}
}
@Test
fun `present - NavigationBar change`() = runTest {
val presenter = createHomePresenter()
val presenter = createHomePresenter(
sessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -152,6 +185,9 @@ class HomePresenterTest {
fun `present - NavigationBar is hidden when the last space is left`() = runTest {
val homeSpacesPresenter = MutablePresenter(aHomeSpacesState())
val presenter = createHomePresenter(
sessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.Space.key to true),
),
@ -185,6 +221,7 @@ internal fun createHomePresenter(
indicatorService: IndicatorService = FakeIndicatorService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
homeSpacesPresenter: Presenter<HomeSpacesState> = Presenter { aHomeSpacesState() },
sessionStore: SessionStore = InMemorySessionStore(),
) = HomePresenter(
client = client,
syncService = syncService,
@ -195,4 +232,5 @@ internal fun createHomePresenter(
homeSpacesPresenter = homeSpacesPresenter,
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
featureFlagService = featureFlagService,
sessionStore = sessionStore,
)

View file

@ -13,7 +13,7 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.architecture.NodeInputs
@ -21,7 +21,7 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class DeclineAndBlockNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -17,7 +17,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.impl.DeclineInvite
import io.element.android.libraries.architecture.AsyncAction
@ -28,7 +28,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Inject
@AssistedInject
class DeclineAndBlockPresenter(
@Assisted private val inviteData: InviteData,
private val declineInvite: DeclineInvite,

View file

@ -3,6 +3,8 @@
<string name="screen_decline_and_block_block_user_option_title">"Блокиране на потребителя"</string>
<string name="screen_invites_decline_chat_message">"Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Отказване на покана"</string>
<string name="screen_invites_decline_direct_chat_message">"Сигурни ли сте, че искате да откажете този личен чат с %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Отказване на чат"</string>
<string name="screen_invites_empty_list">"Няма покани"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) ви покани"</string>
</resources>

View file

@ -4,7 +4,7 @@
<string name="screen_decline_and_block_block_user_option_title">"Felhasználó letiltása"</string>
<string name="screen_decline_and_block_report_user_option_description">"A szoba jelentése a fiókszolgáltatójának."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Írja le a jelentés okát…"</string>
<string name="screen_decline_and_block_title">"Elutasítás és blokkolás"</string>
<string name="screen_decline_and_block_title">"Elutasítás és letiltás"</string>
<string name="screen_invites_decline_chat_message">"Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez: %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Meghívás elutasítása"</string>
<string name="screen_invites_decline_direct_chat_message">"Biztos, hogy elutasítja ezt a privát csevegést vele: %1$s?"</string>

View file

@ -18,8 +18,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
import io.element.android.features.invitepeople.api.InvitePeopleState
@ -51,7 +51,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Inject
@AssistedInject
class DefaultInvitePeoplePresenter(
@Assisted private val joinedRoom: JoinedRoom?,
@Assisted private val roomId: RoomId,

View file

@ -17,7 +17,7 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
@ -30,7 +30,7 @@ import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class JoinRoomFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -21,7 +21,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.SeenInvitesStore
@ -59,7 +59,7 @@ import kotlinx.coroutines.launch
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
@Inject
@AssistedInject
class JoinRoomPresenter(
@Assisted private val roomId: RoomId,
@Assisted private val roomIdOrAlias: RoomIdOrAlias,

View file

@ -15,6 +15,7 @@
<string name="screen_join_room_fail_reason">"Tato místnost je buď určena pouze pro zvané, nebo do ní může být omezen přístup na úrovni prostoru."</string>
<string name="screen_join_room_forget_action">"Zapomenout na tuto místnost"</string>
<string name="screen_join_room_invite_required_message">"Abyste se mohli připojit k této místnosti, potřebujete pozvánku."</string>
<string name="screen_join_room_invited_by">"Pozván(a)"</string>
<string name="screen_join_room_join_action">"Připojit se do místnosti"</string>
<string name="screen_join_room_join_restricted_message">"Abyste se mohli připojit, musíte být pozváni nebo být členem některého prostoru."</string>
<string name="screen_join_room_knock_action">"Zaklepejte a připojte se"</string>

View file

@ -15,6 +15,7 @@
<string name="screen_join_room_fail_reason">"Dette rum er enten kun for gæster, eller der kan være sat begrænsninger for adgangen på klyngeniveau."</string>
<string name="screen_join_room_forget_action">"Glem dette rum"</string>
<string name="screen_join_room_invite_required_message">"Du har brug for en invitation for at deltage i dette rum"</string>
<string name="screen_join_room_invited_by">"Inviteret af"</string>
<string name="screen_join_room_join_action">"Deltag i rummet"</string>
<string name="screen_join_room_join_restricted_message">"Du skal muligvis være inviteret eller være medlem af en klynge for at deltage."</string>
<string name="screen_join_room_knock_action">"Send anmodning om at deltage"</string>

View file

@ -15,6 +15,7 @@
<string name="screen_join_room_fail_reason">"Dieser Chat ist entweder nur auf Einladung zugänglich oder es gibt andere Zugangsbeschränkungen durch Spaces."</string>
<string name="screen_join_room_forget_action">"Vergiss diesen Chat"</string>
<string name="screen_join_room_invite_required_message">"Du benötigst eine Einladung, um diesem Chat beizutreten"</string>
<string name="screen_join_room_invited_by">"Eingeladen von"</string>
<string name="screen_join_room_join_action">"Chat beitreten"</string>
<string name="screen_join_room_join_restricted_message">"Möglicherweise musst du eingeladen werden oder ein Mitglied eines Spaces sein, um beitreten zu können."</string>
<string name="screen_join_room_knock_action">"Anklopfen"</string>

View file

@ -15,6 +15,7 @@
<string name="screen_join_room_fail_reason">"Ligipääs siia jututuppa on võimalik vaid kutse alusel või kehtivad siin kogukonnakohased piirangud."</string>
<string name="screen_join_room_forget_action">"Unusta see jututuba"</string>
<string name="screen_join_room_invite_required_message">"Selle jututoaga liitumiseks vajad sa kutset"</string>
<string name="screen_join_room_invited_by">"Kutsuja"</string>
<string name="screen_join_room_join_action">"Liitu jututoaga"</string>
<string name="screen_join_room_join_restricted_message">"Selle jututoaga liitumiseks sa vajad kutset või pead juba olema kogukonna liige."</string>
<string name="screen_join_room_knock_action">"Liitumiseks koputa jututoa uksele"</string>

View file

@ -15,6 +15,7 @@
<string name="screen_join_room_fail_reason">"Ce salon est accessible uniquement sur invitation ou il peut y avoir des restrictions daccès au niveau de lespace."</string>
<string name="screen_join_room_forget_action">"Oublier ce salon"</string>
<string name="screen_join_room_invite_required_message">"Vous avez besoin dune invitation pour rejoindre ce salon"</string>
<string name="screen_join_room_invited_by">"Invité(e) par"</string>
<string name="screen_join_room_join_action">"Rejoindre"</string>
<string name="screen_join_room_join_restricted_message">"Il est possible que vous deviez être invité ou être membre dun Espace pour pouvoir rejoindre le salon."</string>
<string name="screen_join_room_knock_action">"Demander à joindre"</string>

View file

@ -15,6 +15,7 @@
<string name="screen_join_room_fail_reason">"Ebbe a szobába csak meghívóval vagy tértagsággal lehet belépni."</string>
<string name="screen_join_room_forget_action">"Szoba elfelejtése"</string>
<string name="screen_join_room_invite_required_message">"Meghívóra van szüksége ahhoz, hogy csatlakozzon ehhez a szobához"</string>
<string name="screen_join_room_invited_by">"Meghívta:"</string>
<string name="screen_join_room_join_action">"Csatlakozás a szobához"</string>
<string name="screen_join_room_join_restricted_message">"A csatlakozáshoz meghívásra vagy tértagságra lehet szüksége."</string>
<string name="screen_join_room_knock_action">"Kopogtasson a csatlakozáshoz"</string>

View file

@ -15,6 +15,7 @@
<string name="screen_join_room_fail_reason">"A entrada nesta sala ou está limitada a convites ou a alguma configuração de espaço."</string>
<string name="screen_join_room_forget_action">"Esquecer esta sala"</string>
<string name="screen_join_room_invite_required_message">"Precisas de um convite para entrares nesta sala"</string>
<string name="screen_join_room_invited_by">"Convidado por"</string>
<string name="screen_join_room_join_action">"Entrar na sala"</string>
<string name="screen_join_room_join_restricted_message">"Podes ter que ser convidado ou pertenceres a um espaço para poderes entrar."</string>
<string name="screen_join_room_knock_action">"Bater à porta"</string>

View file

@ -13,12 +13,12 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.RoomScope
@ContributesNode(RoomScope::class)
@Inject
@AssistedInject
class KnockRequestsListNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_room_alert_empty_subtitle">"Сигурни ли сте, че искате да напуснете тази стая? Вие сте единственият човек тук. Ако напуснете, никой няма да може да се присъедини в бъдеще, включително и вие."</string>
<string name="leave_room_alert_private_subtitle">"Сигурни ли сте, че искате да напуснете тази стая? Тази стая не е общодостъпна и няма да можете да се присъедините отново без покана."</string>
<string name="leave_room_alert_subtitle">"Сигурни ли сте, че искате да напуснете стаята?"</string>
</resources>

View file

@ -17,7 +17,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.licenses.impl.details.DependenciesDetailsNode
import io.element.android.features.licenses.impl.list.DependencyLicensesListNode
@ -28,7 +28,7 @@ import io.element.android.libraries.architecture.createNode
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class DependenciesFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -14,14 +14,14 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class DependenciesDetailsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -15,12 +15,12 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class DependencyLicensesListNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -14,11 +14,11 @@ import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
@Suppress("unused")
@Inject
@AssistedInject
class DefaultPermissionsPresenter(
@Assisted private val permissions: List<String>
) : PermissionsPresenter {

View file

@ -14,7 +14,7 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
@Inject
@AssistedInject
class SendLocationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -17,7 +17,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.actions.LocationActions
@ -36,7 +36,7 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.launch
@Inject
@AssistedInject
class SendLocationPresenter(
permissionsPresenterFactory: PermissionsPresenter.Factory,
private val room: JoinedRoom,

View file

@ -14,7 +14,7 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.location.api.ShowLocationEntryPoint
@ -23,7 +23,7 @@ import io.element.android.libraries.di.RoomScope
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
@Inject
@AssistedInject
class ShowLocationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -16,7 +16,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.actions.LocationActions
@ -26,7 +26,7 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
@Inject
@AssistedInject
class ShowLocationPresenter(
@Assisted private val location: Location,
@Assisted private val description: String?,

View file

@ -16,7 +16,7 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode
@ -29,7 +29,7 @@ import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class LockScreenFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -20,7 +20,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
@ -35,7 +35,7 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class LockScreenSettingsFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -14,12 +14,12 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class LockScreenSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -18,7 +18,7 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
@ -32,7 +32,7 @@ import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class LockScreenSetupFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -15,12 +15,12 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class SetupBiometricNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -13,12 +13,12 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class SetupPinNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -15,12 +15,12 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class PinUnlockNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -1,7 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"биометрично удостоверяване"</string>
<string name="screen_app_lock_biometric_unlock">"биометрично отключване"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Отключване с биометрия"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Потвърдете биометричните данни"</string>
<string name="screen_app_lock_forgot_pin">"Забравихте PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Промяна на PIN кода"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Разрешаване на биометрично отключване"</string>
<string name="screen_app_lock_settings_remove_pin">"Премахване на PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Сигурни ли сте, че искате да премахнете PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Премахване на PIN?"</string>
@ -9,6 +14,10 @@
<string name="screen_app_lock_setup_biometric_unlock_skip">"Предпочитам да използвам PIN"</string>
<string name="screen_app_lock_setup_choose_pin">"Избор на PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Потвърждаване на PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Заключете %1$s, за да добавите допълнителна сигурност към вашите чатове.
Изберете нещо запомнящо се. Ако забравите този PIN, ще бъдете излезли от приложението."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Не можете да изберете това за ваш PIN код от съображения за сигурност"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Избор на различен PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Моля, въведете един и същ PIN два пъти"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs не съвпадат"</string>
@ -20,6 +29,7 @@
<item quantity="one">"Грешен PIN. Имате още %1$d шанс"</item>
<item quantity="other">"Грешен PIN. Имате още %1$d шанса"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Използване на биометрия"</string>
<string name="screen_app_lock_use_pin_android">"Използване на PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Излизане…"</string>
</resources>

View file

@ -40,6 +40,7 @@ dependencies {
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.qrcode)
implementation(projects.libraries.oidc.api)
implementation(projects.libraries.uiUtils)
@ -56,5 +57,6 @@ dependencies {
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.libraries.wellknown.test)
}

View file

@ -24,7 +24,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.singleTop
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.login.api.LoginEntryPoint
@ -51,7 +51,7 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class LoginFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
@ -87,7 +87,7 @@ class LoginFlowNode(
// by pressing back or by closing the Custom Chrome Tab.
lifecycleScope.launch {
delay(5000)
oidcActionFlow.post(OidcAction.GoBack)
oidcActionFlow.post(OidcAction.GoBack(toUnblock = true))
}
}
}

View file

@ -94,9 +94,14 @@ class LoginHelper(
}
private suspend fun onOidcAction(oidcAction: OidcAction) {
if (oidcAction is OidcAction.GoBack && oidcAction.toUnblock && loginModeState.value !is AsyncData.Loading) {
// Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode.
// This can happen if there is an error, for instance attempt to login again on the same account.
return
}
loginModeState.value = AsyncData.Loading()
when (oidcAction) {
OidcAction.GoBack -> {
is OidcAction.GoBack -> {
authenticationService.cancelOidcLogin()
.onSuccess {
loginModeState.value = AsyncData.Uninitialized

View file

@ -14,7 +14,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.features.login.impl.error.ChangeServerErrorProvider
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
import io.element.android.libraries.androidutils.system.openGooglePlay
import io.element.android.libraries.architecture.AsyncData
@ -23,6 +22,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
import io.element.android.libraries.matrix.api.auth.AuthenticationException
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.ui.strings.CommonStrings
@ -89,6 +89,12 @@ fun LoginModeView(
onSubmit = onClearError,
)
}
is AuthenticationException.AccountAlreadyLoggedIn -> {
ErrorDialog(
content = stringResource(CommonStrings.error_account_already_logged_in, error.message.orEmpty()),
onSubmit = onClearError,
)
}
else -> {
ErrorDialog(
content = stringResource(CommonStrings.error_unknown),
@ -113,7 +119,7 @@ fun LoginModeView(
@PreviewsDayNight
@Composable
internal fun LoginModeViewPreview(@PreviewParameter(ChangeServerErrorProvider::class) error: ChangeServerError) {
internal fun LoginModeViewPreview(@PreviewParameter(LoginModeViewErrorProvider::class) error: Throwable) {
ElementPreview {
LoginModeView(
loginMode = AsyncData.Failure(error),

View file

@ -0,0 +1,18 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.login
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.error.ChangeServerErrorProvider
import io.element.android.libraries.matrix.api.auth.AuthenticationException
class LoginModeViewErrorProvider : PreviewParameterProvider<Exception> {
override val values: Sequence<Exception>
get() = ChangeServerErrorProvider().values +
AuthenticationException.AccountAlreadyLoggedIn("@alice:matrix.org")
}

View file

@ -23,7 +23,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.di.QrCodeLoginBindings
import io.element.android.features.login.impl.di.QrCodeLoginGraph
@ -49,7 +49,7 @@ import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class QrCodeLoginFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -16,12 +16,12 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class ChangeAccountProviderNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -16,13 +16,13 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.matrix.api.auth.OidcDetails
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class ChooseAccountProviderNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -16,7 +16,7 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.NodeInputs
@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.matrix.api.auth.OidcDetails
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class ConfirmAccountProviderNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -13,12 +13,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.login.LoginHelper
import io.element.android.libraries.architecture.Presenter
@Inject
@AssistedInject
class ConfirmAccountProviderPresenter(
@Assisted private val params: Params,
private val accountProviderDataSource: AccountProviderDataSource,

View file

@ -16,7 +16,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class CreateAccountNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -15,7 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.data.tryOrNull
@ -31,7 +31,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlin.time.Duration.Companion.seconds
@Inject
@AssistedInject
class CreateAccountPresenter(
@Assisted private val url: String,
private val authenticationService: MatrixAuthenticationService,

View file

@ -14,11 +14,11 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class LoginPasswordNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -16,7 +16,7 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.NodeInputs
@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.matrix.api.auth.OidcDetails
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class OnBoardingNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
@ -97,6 +97,7 @@ class OnBoardingNode(
onNeedLoginPassword = ::onLoginPasswordNeeded,
onLearnMoreClick = { openLearnMorePage(context) },
onCreateAccountContinue = ::onCreateAccountContinue,
onBackClick = ::navigateUp,
)
}
}

View file

@ -18,7 +18,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.appconfig.OnBoardingConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
@ -27,9 +27,10 @@ import io.element.android.features.login.impl.login.LoginHelper
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.ui.utils.MultipleTapToUnlock
@Inject
@AssistedInject
class OnBoardingPresenter(
@Assisted private val params: OnBoardingNode.Params,
private val buildMeta: BuildMeta,
@ -38,6 +39,7 @@ class OnBoardingPresenter(
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val loginHelper: LoginHelper,
private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider,
private val sessionStore: SessionStore,
) : Presenter<OnBoardingState> {
@AssistedFactory
interface Factory {
@ -86,6 +88,10 @@ class OnBoardingPresenter(
val onBoardingLogoResId = remember {
onBoardingLogoResIdProvider.get()
}
val isAddingAccount by produceState(initialValue = false) {
// We are adding an account if there is at least one session already stored
value = sessionStore.getAllSessions().isNotEmpty()
}
val loginMode by loginHelper.collectLoginMode()
@ -109,6 +115,7 @@ class OnBoardingPresenter(
}
return OnBoardingState(
isAddingAccount = isAddingAccount,
productionApplicationName = buildMeta.productionApplicationName,
defaultAccountProvider = defaultAccountProvider,
mustChooseAccountProvider = mustChooseAccountProvider,

View file

@ -12,6 +12,7 @@ import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
data class OnBoardingState(
val isAddingAccount: Boolean,
val productionApplicationName: String,
val defaultAccountProvider: String?,
val mustChooseAccountProvider: Boolean,

View file

@ -23,10 +23,16 @@ open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true, canReportBug = true),
anOnBoardingState(defaultAccountProvider = "element.io", canCreateAccount = false, canReportBug = true),
anOnBoardingState(customLogoResId = R.drawable.sample_background),
anOnBoardingState(
isAddingAccount = true,
canLoginWithQrCode = true,
canCreateAccount = true,
),
)
}
fun anOnBoardingState(
isAddingAccount: Boolean = false,
productionApplicationName: String = "Element",
defaultAccountProvider: String? = null,
mustChooseAccountProvider: Boolean = false,
@ -39,6 +45,7 @@ fun anOnBoardingState(
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
eventSink: (OnBoardingEvents) -> Unit = {},
) = OnBoardingState(
isAddingAccount = isAddingAccount,
productionApplicationName = productionApplicationName,
defaultAccountProvider = defaultAccountProvider,
mustChooseAccountProvider = mustChooseAccountProvider,

View file

@ -38,7 +38,9 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage
import io.element.android.libraries.designsystem.components.BigIcon
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
@ -58,6 +60,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun OnBoardingView(
state: OnBoardingState,
onBackClick: () -> Unit,
onSignInWithQrCode: () -> Unit,
onSignIn: (mustChooseAccountProvider: Boolean) -> Unit,
onCreateAccount: () -> Unit,
@ -67,6 +70,52 @@ fun OnBoardingView(
onCreateAccountContinue: (url: String) -> Unit,
onReportProblem: () -> Unit,
modifier: Modifier = Modifier,
) {
val loginView = @Composable {
LoginModeView(
loginMode = state.loginMode,
onClearError = {
state.eventSink(OnBoardingEvents.ClearError)
},
onLearnMoreClick = onLearnMoreClick,
onOidcDetails = onOidcDetails,
onNeedLoginPassword = onNeedLoginPassword,
onCreateAccountContinue = onCreateAccountContinue,
)
}
val buttons = @Composable {
OnBoardingButtons(
state = state,
onSignInWithQrCode = onSignInWithQrCode,
onSignIn = onSignIn,
onCreateAccount = onCreateAccount,
onReportProblem = onReportProblem,
)
}
if (state.isAddingAccount) {
AddOtherAccountScaffold(
modifier = modifier,
loginView = loginView,
buttons = buttons,
onBackClick = onBackClick,
)
} else {
AddFirstAccountScaffold(
modifier = modifier,
state = state,
loginView = loginView,
buttons = buttons,
)
}
}
@Composable
private fun AddFirstAccountScaffold(
state: OnBoardingState,
loginView: @Composable () -> Unit,
buttons: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
OnBoardingPage(
modifier = modifier,
@ -79,29 +128,31 @@ fun OnBoardingView(
} else {
OnBoardingContent(state = state)
}
LoginModeView(
loginMode = state.loginMode,
onClearError = {
state.eventSink(OnBoardingEvents.ClearError)
},
onLearnMoreClick = onLearnMoreClick,
onOidcDetails = onOidcDetails,
onNeedLoginPassword = onNeedLoginPassword,
onCreateAccountContinue = onCreateAccountContinue,
)
loginView()
},
footer = {
OnBoardingButtons(
state = state,
onSignInWithQrCode = onSignInWithQrCode,
onSignIn = onSignIn,
onCreateAccount = onCreateAccount,
onReportProblem = onReportProblem,
)
buttons()
}
)
}
@Composable
private fun AddOtherAccountScaffold(
loginView: @Composable () -> Unit,
buttons: @Composable () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
FlowStepPage(
modifier = modifier,
title = stringResource(CommonStrings.common_add_account),
iconStyle = BigIcon.Style.Default(CompoundIcons.HomeSolid()),
buttons = { buttons() },
content = loginView,
onBackClick = onBackClick,
)
}
@Composable
private fun OnBoardingContent(state: OnBoardingState) {
Box(
@ -226,27 +277,29 @@ private fun OnBoardingButtons(
.fillMaxWidth()
)
}
if (state.canReportBug) {
// Add a report problem text button. Use a Text since we need a special theme here.
Text(
modifier = Modifier
.clickable(onClick = onReportProblem)
.padding(16.dp),
text = stringResource(id = CommonStrings.common_report_a_problem),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
} else {
Text(
modifier = Modifier
.clickable {
state.eventSink(OnBoardingEvents.OnVersionClick)
}
.padding(16.dp),
text = stringResource(id = R.string.screen_onboarding_app_version, state.version),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
if (state.isAddingAccount.not()) {
if (state.canReportBug) {
// Add a report problem text button. Use a Text since we need a special theme here.
Text(
modifier = Modifier
.clickable(onClick = onReportProblem)
.padding(16.dp),
text = stringResource(id = CommonStrings.common_report_a_problem),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
} else {
Text(
modifier = Modifier
.clickable {
state.eventSink(OnBoardingEvents.OnVersionClick)
}
.padding(16.dp),
text = stringResource(id = R.string.screen_onboarding_app_version, state.version),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
}
}
}
}
@ -258,6 +311,7 @@ internal fun OnBoardingViewPreview(
) = ElementPreview {
OnBoardingView(
state = state,
onBackClick = {},
onSignInWithQrCode = {},
onSignIn = {},
onCreateAccount = {},

View file

@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.di.QrCodeLoginScope
import io.element.android.libraries.architecture.inputs
@ContributesNode(QrCodeLoginScope::class)
@Inject
@AssistedInject
class QrCodeConfirmationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -14,7 +14,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.di.QrCodeLoginScope
import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType
@ -22,7 +22,7 @@ import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.core.meta.BuildMeta
@ContributesNode(QrCodeLoginScope::class)
@Inject
@AssistedInject
class QrCodeErrorNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -14,12 +14,12 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.di.QrCodeLoginScope
@ContributesNode(QrCodeLoginScope::class)
@Inject
@AssistedInject
class QrCodeIntroNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.di.QrCodeLoginScope
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
@ContributesNode(QrCodeLoginScope::class)
@Inject
@AssistedInject
class QrCodeScanNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -16,12 +16,12 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage
@ContributesNode(AppScope::class)
@Inject
@AssistedInject
class SearchAccountProviderNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Промяна на доставчика на акаунт"</string>
<string name="screen_account_provider_form_hint">"Адрес на сървъра"</string>
<string name="screen_account_provider_form_notice">"Въведете термин за търсене или адрес на домейн."</string>
<string name="screen_account_provider_form_subtitle">"Потърсете компания, общност или частен сървър."</string>
<string name="screen_account_provider_form_title">"Намерете доставчик на акаунт"</string>
@ -8,13 +9,19 @@
<string name="screen_account_provider_signin_title">"На път сте да влезете в %s"</string>
<string name="screen_account_provider_signup_subtitle">"Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли."</string>
<string name="screen_account_provider_signup_title">"На път сте да създадете акаунт в %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org е голям, безплатен сървър в публичната мрежа на Matrix за сигурна, децентрализирана комуникация, управляван от фондация Matrix.org."</string>
<string name="screen_change_account_provider_other">"Друг"</string>
<string name="screen_change_account_provider_subtitle">"Използвайте друг доставчик на акаунт, като например собствен частен сървър или работен акаунт."</string>
<string name="screen_change_account_provider_title">"Промяна на доставчика на акаунт"</string>
<string name="screen_change_server_error_invalid_homeserver">"Не можахме да достигнем този сървър. Моля, проверете дали сте въвели правилно URL адреса на сървъра. Ако URL адресът е правилен, свържете се с администратора на вашия сървър за допълнителна помощ."</string>
<string name="screen_change_server_form_header">"URL адрес на сървъра"</string>
<string name="screen_change_server_subtitle">"Какъв е адресът на вашия сървър?"</string>
<string name="screen_change_server_title">"Изберете своя сървър"</string>
<string name="screen_create_account_title">"Създаване на акаунт"</string>
<string name="screen_login_error_deactivated_account">"Този акаунт бе деактивиран."</string>
<string name="screen_login_error_invalid_credentials">"Неправилно потребителско име и/или парола"</string>
<string name="screen_login_error_invalid_user_id">"Това не е валиден потребителски идентификатор. Очакван формат: @user:homeserver.org"</string>
<string name="screen_login_error_unsupported_authentication">"Избраният сървър не поддържа влизане с парола или OIDC. Моля, свържете се с вашия администратор или изберете друг сървър."</string>
<string name="screen_login_form_header">"Въведете своите данни"</string>
<string name="screen_login_subtitle">"Matrix е отворена мрежа за сигурна, децентрализирана комуникация."</string>
<string name="screen_login_title">"Добре дошли отново!"</string>
@ -28,6 +35,7 @@
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторен опит"</string>
<string name="screen_qr_code_login_verify_code_title">"Вашият код за потвърждение"</string>
<string name="screen_server_confirmation_change_server">"Промяна на доставчика на акаунт"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Частен сървър за служителите на Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix е отворена мрежа за сигурна, децентрализирана комуникация."</string>
<string name="screen_server_confirmation_message_register">"Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли."</string>
<string name="screen_server_confirmation_title_login">"На път сте да влезете в %1$s"</string>

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_qr_code_login_connection_note_secure_state_description">"A secure connection could not be made to the new device. Your existing connected devices are still safe and you don\'t need to worry about them."</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"A secure connection could not be made to the new device. Your existing linked devices are still safe and you don\'t need to worry about them."</string>
</resources>

View file

@ -117,7 +117,7 @@ class ConfirmAccountProviderPresenterTest {
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
authenticationService.givenOidcCancelError(AN_EXCEPTION)
defaultOidcActionFlow.post(OidcAction.GoBack)
defaultOidcActionFlow.post(OidcAction.GoBack())
val cancelFailureState = awaitItem()
assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
}
@ -144,7 +144,30 @@ class ConfirmAccountProviderPresenterTest {
assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
defaultOidcActionFlow.post(OidcAction.GoBack)
defaultOidcActionFlow.post(OidcAction.GoBack())
val cancelFinalState = awaitItem()
assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
}
}
@Test
fun `present - oidc - cancel to unblock`() = runTest {
val authenticationService = FakeMatrixAuthenticationService()
val defaultOidcActionFlow = FakeOidcActionFlow()
val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService,
defaultOidcActionFlow = defaultOidcActionFlow,
)
authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
defaultOidcActionFlow.post(OidcAction.GoBack(toUnblock = true))
val cancelFinalState = awaitItem()
assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
}

View file

@ -29,6 +29,9 @@ import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationSer
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.oidc.api.OidcActionFlow
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
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.libraries.wellknown.api.WellknownRetriever
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
@ -79,10 +82,27 @@ class OnBoardingPresenterTest {
assertThat(initialState.productionApplicationName).isEqualTo("B")
assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT)
assertThat(initialState.canReportBug).isFalse()
assertThat(initialState.isAddingAccount).isFalse()
assertThat(awaitItem().canLoginWithQrCode).isTrue()
}
}
@Test
fun `present - initial state adding account`() = runTest {
val presenter = createPresenter(
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData()
)
)
)
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.isAddingAccount).isTrue()
}
}
@Test
fun `present - on boarding logo`() = runTest {
val presenter = createPresenter(
@ -236,6 +256,7 @@ private fun createPresenter(
rageshakeFeatureAvailability: () -> Flow<Boolean> = { flowOf(true) },
loginHelper: LoginHelper = createLoginHelper(),
onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null },
sessionStore: SessionStore = InMemorySessionStore(),
) = OnBoardingPresenter(
params = params,
buildMeta = buildMeta,
@ -247,6 +268,7 @@ private fun createPresenter(
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
loginHelper = loginHelper,
onBoardingLogoResIdProvider = onBoardingLogoResIdProvider,
sessionStore = sessionStore,
)
fun createLoginHelper(

View file

@ -25,6 +25,7 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
@ -50,6 +51,21 @@ class OnboardingViewTest {
}
}
@Test
fun `when can go back - clicking on back calls the expected callback`() {
val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false)
ensureCalledOnce { callback ->
rule.setOnboardingView(
state = anOnBoardingState(
isAddingAccount = true,
eventSink = eventSink,
),
onBackClick = callback,
)
rule.pressBack()
}
}
@Test
fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() {
val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false)
@ -235,6 +251,7 @@ class OnboardingViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOnboardingView(
state: OnBoardingState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onSignInWithQrCode: () -> Unit = EnsureNeverCalled(),
onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
onCreateAccount: () -> Unit = EnsureNeverCalled(),
@ -247,6 +264,7 @@ class OnboardingViewTest {
setContent {
OnBoardingView(
state = state,
onBackClick = onBackClick,
onSignInWithQrCode = onSignInWithQrCode,
onSignIn = onSignIn,
onCreateAccount = onCreateAccount,

View file

@ -14,13 +14,13 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.logout.api.LogoutEntryPoint
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@Inject
@AssistedInject
class LogoutNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -20,7 +20,7 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.annotations.ContributesNode
import io.element.android.features.call.api.CallType
@ -91,7 +91,7 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
@Inject
@AssistedInject
class MessagesFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -25,7 +25,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.knockrequests.api.banner.KnockRequestsBannerRenderer
@ -74,7 +74,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ContributesNode(RoomScope::class)
@Inject
@AssistedInject
class MessagesNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

View file

@ -24,7 +24,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.LifecycleResumeEffect
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.appconfig.MessageComposerConfig
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
@ -57,6 +57,7 @@ import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.components.avatar.AvatarData
@ -70,6 +71,7 @@ import io.element.android.libraries.matrix.api.core.toThreadId
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.recentemojis.AddRecentEmoji
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomInfo
@ -93,7 +95,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
@Inject
@AssistedInject
class MessagesPresenter(
@Assisted private val navigator: MessagesNavigator,
private val room: JoinedRoom,
@ -121,6 +123,7 @@ class MessagesPresenter(
private val analyticsService: AnalyticsService,
private val encryptionService: EncryptionService,
private val featureFlagService: FeatureFlagService,
private val addRecentEmoji: AddRecentEmoji,
) : Presenter<MessagesState> {
@AssistedFactory
interface Factory {
@ -282,8 +285,8 @@ class MessagesPresenter(
}
return produceState(UserEventPermissions.DEFAULT, key1 = key) {
value = UserEventPermissions(
canSendMessage = room.canSendMessage(type = MessageEventType.ROOM_MESSAGE).getOrElse { true },
canSendReaction = room.canSendMessage(type = MessageEventType.REACTION).getOrElse { true },
canSendMessage = room.canSendMessage(type = MessageEventType.RoomMessage).getOrElse { true },
canSendReaction = room.canSendMessage(type = MessageEventType.Reaction).getOrElse { true },
canRedactOwn = room.canRedactOwn().getOrElse { false },
canRedactOther = room.canRedactOther().getOrElse { false },
canPinUnpin = room.canPinUnpin().getOrElse { false },
@ -398,6 +401,7 @@ class MessagesPresenter(
) = launch(dispatchers.io) {
timelineController.invokeOnCurrentTimeline {
toggleReaction(emoji, eventOrTransactionId)
.flatMap { added -> if (added) addRecentEmoji(emoji) else Result.success(Unit) }
.onFailure { Timber.e(it) }
}
}

View file

@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
@ -178,9 +179,11 @@ fun aReactionSummaryState(
fun aCustomReactionState(
target: CustomReactionState.Target = CustomReactionState.Target.None,
recentEmojis: ImmutableList<String> = persistentListOf(),
eventSink: (CustomReactionEvents) -> Unit = {},
) = CustomReactionState(
target = target,
recentEmojis = recentEmojis,
selectedEmoji = persistentSetOf(),
eventSink = eventSink,
)

View file

@ -16,8 +16,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.features.messages.impl.UserEventPermissions
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionComparator
@ -43,6 +43,7 @@ import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.recentemojis.GetRecentEmojis
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@ -62,7 +63,7 @@ interface ActionListPresenter : Presenter<ActionListState> {
}
}
@Inject
@AssistedInject
class DefaultActionListPresenter(
@Assisted
private val postProcessor: TimelineItemActionPostProcessor,
@ -73,6 +74,7 @@ class DefaultActionListPresenter(
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
private val dateFormatter: DateFormatter,
private val featureFlagService: FeatureFlagService,
private val getRecentEmojis: GetRecentEmojis,
) : ActionListPresenter {
@AssistedFactory
@ContributesBinding(RoomScope::class)
@ -153,14 +155,15 @@ class DefaultActionListPresenter(
),
displayEmojiReactions = displayEmojiReactions,
verifiedUserSendFailure = verifiedUserSendFailure,
actions = actions.toImmutableList()
actions = actions.toImmutableList(),
recentEmojis = getRecentEmojis().getOrNull()?.toImmutableList() ?: persistentListOf()
)
} else {
target.value = ActionListState.Target.None
}
}
private suspend fun buildActions(
private fun buildActions(
timelineItem: TimelineItem.Event,
usersEventPermissions: UserEventPermissions,
isDeveloperModeEnabled: Boolean,

View file

@ -26,6 +26,7 @@ data class ActionListState(
val event: TimelineItem.Event,
val sentTimeFull: String,
val displayEmojiReactions: Boolean,
val recentEmojis: ImmutableList<String>,
val verifiedUserSendFailure: VerifiedUserSendFailure,
val actions: ImmutableList<TimelineItemAction>,
) : Target

View file

@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
@ -41,6 +42,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
recentEmojis = persistentListOf(),
)
),
anActionListState(
@ -56,6 +58,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
actions = aTimelineItemActionList(
copyAction = TimelineItemAction.CopyCaption,
),
recentEmojis = persistentListOf(),
)
),
anActionListState(
@ -70,6 +73,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
actions = aTimelineItemActionList(
copyAction = TimelineItemAction.CopyCaption,
),
recentEmojis = persistentListOf(),
)
),
anActionListState(
@ -84,6 +88,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
actions = aTimelineItemActionList(
copyAction = null,
),
recentEmojis = persistentListOf(),
)
),
anActionListState(
@ -98,6 +103,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
actions = aTimelineItemActionList(
copyAction = TimelineItemAction.CopyCaption,
),
recentEmojis = persistentListOf(),
)
),
anActionListState(
@ -112,6 +118,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
actions = aTimelineItemActionList(
copyAction = null,
),
recentEmojis = persistentListOf(),
)
),
anActionListState(
@ -124,6 +131,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
recentEmojis = persistentListOf(),
)
),
anActionListState(
@ -136,6 +144,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
recentEmojis = persistentListOf(),
),
),
anActionListState(
@ -148,6 +157,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemPollActionList(),
recentEmojis = persistentListOf(),
),
),
anActionListState(
@ -160,6 +170,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
recentEmojis = persistentListOf(),
)
),
anActionListState(
@ -169,6 +180,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
displayEmojiReactions = true,
verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
actions = aTimelineItemActionList(),
recentEmojis = persistentListOf(),
)
),
)

View file

@ -13,6 +13,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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -20,9 +21,11 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
@ -35,6 +38,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@ -90,6 +97,8 @@ import io.element.android.libraries.matrix.ui.messages.sender.SenderName
import io.element.android.libraries.matrix.ui.messages.sender.SenderNameMode
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -218,6 +227,7 @@ private fun ActionListViewContent(
if (target.displayEmojiReactions) {
item {
EmojiReactionsRow(
recentEmojis = target.recentEmojis,
highlightedEmojis = target.event.reactionsState.highlightedKeys,
onEmojiReactionClick = onEmojiReactionClick,
onCustomReactionClick = onCustomReactionClick,
@ -335,43 +345,67 @@ private fun MessageSummary(
}
private val emojiRippleRadius = 24.dp
private val suggestedEmojis = persistentListOf("👍️", "👎️", "🔥", "❤️", "👏")
@Composable
private fun EmojiReactionsRow(
recentEmojis: ImmutableList<String>,
highlightedEmojis: ImmutableList<String>,
onEmojiReactionClick: (String) -> Unit,
onCustomReactionClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = modifier.padding(horizontal = 24.dp, vertical = 16.dp)
modifier = modifier.padding(end = 16.dp, top = 16.dp, bottom = 16.dp),
) {
// TODO use most recently used emojis here when available from the Rust SDK
val defaultEmojis = sequenceOf(
"👍️",
"👎️",
"🔥",
"❤️",
"👏"
)
for (emoji in defaultEmojis) {
val isHighlighted = highlightedEmojis.contains(emoji)
EmojiButton(
modifier = Modifier
// Make it appear after the more useful actions for the accessibility service
.semantics {
traversalIndex = 1f
},
emoji = emoji,
isHighlighted = isHighlighted,
onClick = onEmojiReactionClick
)
val backgroundColor = ElementTheme.colors.bgCanvasDefault
val emojis = remember(recentEmojis) {
(suggestedEmojis + recentEmojis.filter { it !in suggestedEmojis })
.take(100)
.toImmutableList()
}
Box(
LazyRow(
modifier = Modifier
.size(48.dp),
contentAlignment = Alignment.Center,
.weight(1f, fill = true)
.drawWithContent {
val gradientWidth = 24.dp.toPx()
val width = size.width
drawContent()
drawRect(
brush = Brush.horizontalGradient(
0.0f to Color.Transparent,
1.0f to backgroundColor,
startX = width - gradientWidth,
endX = width,
),
topLeft = Offset(width - gradientWidth, 0f),
size = Size(gradientWidth, size.height)
)
},
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items(emojis) { emoji ->
val isHighlighted = highlightedEmojis.contains(emoji)
EmojiButton(
modifier = Modifier
// Make it appear after the more useful actions for the accessibility service
.semantics {
traversalIndex = 1f
},
emoji = emoji,
isHighlighted = isHighlighted,
onClick = onEmojiReactionClick
)
}
}
Box(
modifier = Modifier.padding(end = 10.dp).requiredSize(48.dp),
contentAlignment = Alignment.CenterEnd,
) {
Icon(
imageVector = CompoundIcons.ReactionAdd(),

View file

@ -13,7 +13,7 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ForcedDarkElementTheme
import io.element.android.features.messages.impl.attachments.Attachment
@ -25,7 +25,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer
@ContributesNode(RoomScope::class)
@Inject
@AssistedInject
class AttachmentsPreviewNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

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