mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Merge pull request #7328 from element-hq/feature/fga/user_status_last_pieces
User status last pieces
This commit is contained in:
+127
-5
@@ -8,6 +8,8 @@
|
||||
|
||||
package io.element.android.features.home.impl.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
@@ -15,9 +17,11 @@ import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.pager.VerticalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
@@ -25,8 +29,11 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -37,10 +44,20 @@ 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.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.appconfig.RoomListConfig
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
@@ -71,10 +88,13 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.toEmojiText
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -151,7 +171,7 @@ fun HomeTopBar(
|
||||
},
|
||||
// We want a 16dp left padding for the navigationIcon :
|
||||
// 4dp from default TopAppBarHorizontalPadding
|
||||
// 8dp from AccountIcon default padding (because of IconButton)
|
||||
// 8dp from AccountIcon default padding (48dp touch target around 32dp avatar)
|
||||
// 4dp extra padding using left insets
|
||||
windowInsets = WindowInsets(left = 4.dp),
|
||||
)
|
||||
@@ -316,19 +336,36 @@ private fun AccountIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val testTag = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier
|
||||
IconButton(
|
||||
modifier = modifier.then(testTag),
|
||||
onClick = onClick,
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Box(
|
||||
modifier = modifier
|
||||
.then(testTag)
|
||||
.minimumInteractiveComponentSize()
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
indication = ripple(bounded = false),
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box {
|
||||
val avatarData by remember(matrixUser) {
|
||||
derivedStateOf {
|
||||
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||
}
|
||||
}
|
||||
val statusEmoji = matrixUser.displayedStatus?.toEmojiText()
|
||||
val avatarModifier = if (statusEmoji != null) {
|
||||
Modifier.eraseStatusEmojiBackground(
|
||||
parentSize = AvatarSize.CurrentUserTopBar.dp,
|
||||
layoutDirection = LocalLayoutDirection.current,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.User,
|
||||
modifier = avatarModifier,
|
||||
contentDescription = if (isCurrentAccount) {
|
||||
if (showAvatarIndicator) {
|
||||
stringResource(CommonStrings.a11y_settings_with_required_action)
|
||||
@@ -339,11 +376,69 @@ private fun AccountIcon(
|
||||
null
|
||||
},
|
||||
)
|
||||
if (statusEmoji != null) {
|
||||
StatusEmojiBadge(
|
||||
emoji = statusEmoji,
|
||||
modifier = Modifier.align(Alignment.BottomEnd),
|
||||
)
|
||||
}
|
||||
if (showAvatarIndicator) {
|
||||
RedIndicatorAtom(
|
||||
modifier = Modifier.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val statusEmojiBadgeSize = 20.dp
|
||||
private val statusEmojiBadgeRadius = statusEmojiBadgeSize / 2
|
||||
private val statusEmojiBadgeOffset = 8.dp
|
||||
|
||||
private fun Modifier.eraseStatusEmojiBackground(
|
||||
parentSize: Dp,
|
||||
layoutDirection: LayoutDirection,
|
||||
): Modifier = this
|
||||
.graphicsLayer {
|
||||
compositingStrategy = CompositingStrategy.Offscreen
|
||||
}
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawCircle(
|
||||
color = Color.Black,
|
||||
center = Offset(
|
||||
x = if (layoutDirection == LayoutDirection.Ltr) {
|
||||
(parentSize - statusEmojiBadgeRadius + statusEmojiBadgeOffset).toPx()
|
||||
} else {
|
||||
(statusEmojiBadgeRadius - statusEmojiBadgeOffset).toPx()
|
||||
},
|
||||
y = size.height - statusEmojiBadgeRadius.toPx(),
|
||||
),
|
||||
radius = statusEmojiBadgeRadius.toPx(),
|
||||
blendMode = BlendMode.Clear,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusEmojiBadge(
|
||||
emoji: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
// Ensure the emoji size is not affected by the user's font scale settings
|
||||
val fixedFontScaleDensity = remember(density.density) {
|
||||
Density(density = density.density, fontScale = 1f)
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
.offset(x = statusEmojiBadgeOffset)
|
||||
.size(statusEmojiBadgeSize),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CompositionLocalProvider(LocalDensity provides fixedFontScaleDensity) {
|
||||
Text(
|
||||
text = emoji,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,6 +527,33 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun HomeTopBarWithStatusPreview() = ElementPreview {
|
||||
HomeTopBar(
|
||||
selectedNavigationItem = HomeNavigationBarItem.Chats,
|
||||
currentUserAndNeighbors = persistentListOf(
|
||||
aMatrixUser(
|
||||
id = "@id:domain",
|
||||
displayName = USER_NAME_ALICE,
|
||||
displayedStatus = DisplayedStatus.UserSet(UserStatus(emoji = "🌴", text = "Away")),
|
||||
)
|
||||
),
|
||||
showAvatarIndicator = false,
|
||||
areSearchResultsDisplayed = false,
|
||||
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
|
||||
onOpenSettings = {},
|
||||
onAccountSwitch = {},
|
||||
onToggleSearch = {},
|
||||
canReportBug = true,
|
||||
displayFilters = true,
|
||||
filtersState = aRoomListFiltersState(),
|
||||
spaceFiltersState = anUnselectedSpaceFiltersState(),
|
||||
onMenuActionClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
|
||||
+2
-5
@@ -125,6 +125,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.mediaSources
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.toMatrixUser
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.media.contentvalidation.collectOverallState
|
||||
import io.element.android.libraries.matrix.ui.media.contentvalidation.rememberEventContentValidationState
|
||||
@@ -207,11 +208,7 @@ fun TimelineItemEventRow(
|
||||
}
|
||||
|
||||
fun onUserDataClick() {
|
||||
val sender = MatrixUser(
|
||||
userId = event.senderId,
|
||||
displayName = event.senderProfile.getDisplayName(),
|
||||
avatarUrl = event.senderProfile.getAvatarUrl(),
|
||||
)
|
||||
val sender = event.senderProfile.toMatrixUser(event.senderId)
|
||||
onUserDataClick(sender)
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -74,8 +74,14 @@ class PreferencesRootPresenter(
|
||||
val showLinkNewDevice by remember {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.QrCodeLogin)
|
||||
}.collectAsState(initial = false)
|
||||
val isUserStatusEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus).collectAsState(initial = false)
|
||||
val userStatusState = if (isUserStatusEnabled) userStatusPresenter.present() else null
|
||||
|
||||
val isUserStatusSupported by produceState(false) {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus)
|
||||
.collect { isUserStatusEnabled ->
|
||||
value = isUserStatusEnabled && matrixClient.isUserStatusSupported().getOrDefault(false)
|
||||
}
|
||||
}
|
||||
val userStatusState = if (isUserStatusSupported) userStatusPresenter.present() else null
|
||||
|
||||
val otherSessions by remember {
|
||||
sessionStore.sessionsFlow().map { list ->
|
||||
|
||||
+82
-60
@@ -9,9 +9,12 @@
|
||||
package io.element.android.features.preferences.impl.root
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -25,7 +28,10 @@ import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.features.preferences.impl.user.UserPreferences
|
||||
import io.element.android.features.preferences.impl.userstatus.UserStatusState
|
||||
import io.element.android.features.preferences.impl.userstatus.UserStatusView
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionIndicator
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||
@@ -71,71 +77,87 @@ fun PreferencesRootView(
|
||||
) {
|
||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||
|
||||
// Include pref from other modules
|
||||
PreferencePage(
|
||||
modifier = modifier,
|
||||
onBackClick = onBackClick,
|
||||
title = stringResource(id = CommonStrings.common_settings),
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) {
|
||||
UserPreferences(
|
||||
modifier = Modifier.clickable {
|
||||
onOpenUserProfile(state.myUser)
|
||||
},
|
||||
matrixUser = state.myUser,
|
||||
)
|
||||
if (state.isMultiAccountEnabled) {
|
||||
MultiAccountSection(
|
||||
state = state,
|
||||
onAddAccountClick = onAddAccountClick,
|
||||
Box(modifier = modifier) {
|
||||
// Include pref from other modules
|
||||
PreferencePage(
|
||||
onBackClick = onBackClick,
|
||||
title = stringResource(id = CommonStrings.common_settings),
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) {
|
||||
UserPreferences(
|
||||
modifier = Modifier.clickable {
|
||||
onOpenUserProfile(state.myUser)
|
||||
},
|
||||
matrixUser = state.myUser,
|
||||
)
|
||||
}
|
||||
if (state.userStatusState != null) {
|
||||
UserStatusSection(
|
||||
userStatusState = state.userStatusState,
|
||||
emojiPickerRenderer = emojiPickerRenderer,
|
||||
showTopDivider = !state.isMultiAccountEnabled,
|
||||
)
|
||||
}
|
||||
// 'Account' section
|
||||
ManageAccountSection(
|
||||
state = state,
|
||||
onManageAccountClick = onManageAccountClick,
|
||||
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
||||
onOpenBlockedUsers = onOpenBlockedUsers
|
||||
)
|
||||
// 'Manage my app' section
|
||||
ManageAppSection(
|
||||
state = state,
|
||||
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||
onOpenLockScreenSettings = onOpenLockScreenSettings,
|
||||
onSecureBackupClick = onSecureBackupClick,
|
||||
)
|
||||
|
||||
// General section
|
||||
GeneralSection(
|
||||
state = state,
|
||||
onOpenAbout = onOpenAbout,
|
||||
onOpenAnalytics = onOpenAnalytics,
|
||||
onOpenRageShake = onOpenRageShake,
|
||||
onOpenAdvancedSettings = onOpenAdvancedSettings,
|
||||
onOpenDeveloperSettings = onOpenDeveloperSettings,
|
||||
onOpenLabs = onOpenLabs,
|
||||
onSignOutClick = onSignOutClick,
|
||||
onDeactivateClick = onDeactivateClick,
|
||||
)
|
||||
// Version
|
||||
Footer(
|
||||
version = state.version,
|
||||
onClick = if (!state.showDeveloperSettings) {
|
||||
{ state.eventSink(PreferencesRootEvent.OnVersionInfoClick) }
|
||||
} else {
|
||||
null
|
||||
if (state.isMultiAccountEnabled) {
|
||||
MultiAccountSection(
|
||||
state = state,
|
||||
onAddAccountClick = onAddAccountClick,
|
||||
)
|
||||
}
|
||||
)
|
||||
if (state.userStatusState != null) {
|
||||
UserStatusSection(
|
||||
userStatusState = state.userStatusState,
|
||||
emojiPickerRenderer = emojiPickerRenderer,
|
||||
showTopDivider = !state.isMultiAccountEnabled,
|
||||
)
|
||||
}
|
||||
// 'Account' section
|
||||
ManageAccountSection(
|
||||
state = state,
|
||||
onManageAccountClick = onManageAccountClick,
|
||||
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
||||
onOpenBlockedUsers = onOpenBlockedUsers
|
||||
)
|
||||
// 'Manage my app' section
|
||||
ManageAppSection(
|
||||
state = state,
|
||||
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||
onOpenLockScreenSettings = onOpenLockScreenSettings,
|
||||
onSecureBackupClick = onSecureBackupClick,
|
||||
)
|
||||
|
||||
// General section
|
||||
GeneralSection(
|
||||
state = state,
|
||||
onOpenAbout = onOpenAbout,
|
||||
onOpenAnalytics = onOpenAnalytics,
|
||||
onOpenRageShake = onOpenRageShake,
|
||||
onOpenAdvancedSettings = onOpenAdvancedSettings,
|
||||
onOpenDeveloperSettings = onOpenDeveloperSettings,
|
||||
onOpenLabs = onOpenLabs,
|
||||
onSignOutClick = onSignOutClick,
|
||||
onDeactivateClick = onDeactivateClick,
|
||||
)
|
||||
// Version
|
||||
Footer(
|
||||
version = state.version,
|
||||
onClick = if (!state.showDeveloperSettings) {
|
||||
{ state.eventSink(PreferencesRootEvent.OnVersionInfoClick) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
state.userStatusState?.let {
|
||||
UserStatusUpdateIndicator(it.updateStatusAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.UserStatusUpdateIndicator(updateStatusAction: AsyncAction<Unit>) {
|
||||
AsyncActionIndicator(
|
||||
asyncAction = updateStatusAction,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.statusBarsPadding(),
|
||||
loading = { AsyncIndicator.Loading(text = stringResource(CommonStrings.common_saving)) },
|
||||
failure = { _ -> AsyncIndicator.Failure(text = stringResource(CommonStrings.common_failed)) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.UserStatusSection(
|
||||
userStatusState: UserStatusState,
|
||||
|
||||
+10
-2
@@ -18,7 +18,9 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter
|
||||
import io.element.android.libraries.emoji.api.recentemojis.EmptyGetRecentEmojis
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
@@ -38,6 +40,7 @@ class UserStatusPresenter(
|
||||
var isEmojiPickerVisible by remember { mutableStateOf(false) }
|
||||
val customTextFieldState = rememberTextFieldState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val updateStatusAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
fun handleEvent(event: UserStatusEvent) {
|
||||
when (event) {
|
||||
@@ -47,11 +50,15 @@ class UserStatusPresenter(
|
||||
UserStatusEvent.CancelCustomInput -> pickerState = UserStatusPickerState.Hidden
|
||||
is UserStatusEvent.SetStatus -> {
|
||||
pickerState = UserStatusPickerState.Hidden
|
||||
coroutineScope.launch { matrixClient.setUserStatus(event.status) }
|
||||
coroutineScope.launch {
|
||||
updateStatusAction.runUpdatingState { matrixClient.setUserStatus(event.status) }
|
||||
}
|
||||
}
|
||||
UserStatusEvent.ClearStatus -> {
|
||||
pickerState = UserStatusPickerState.Hidden
|
||||
coroutineScope.launch { matrixClient.clearUserStatus() }
|
||||
coroutineScope.launch {
|
||||
updateStatusAction.runUpdatingState { matrixClient.clearUserStatus() }
|
||||
}
|
||||
}
|
||||
UserStatusEvent.OpenCustomInput -> {
|
||||
val raw = userProfile.rawStatus
|
||||
@@ -93,6 +100,7 @@ class UserStatusPresenter(
|
||||
displayedStatus = userProfile.displayedStatus,
|
||||
rawStatus = userProfile.rawStatus,
|
||||
pickerState = effectivePickerState,
|
||||
updateStatusAction = updateStatusAction.value,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.emoji.api.picker.EmojiPickerState
|
||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||
@@ -19,6 +20,7 @@ data class UserStatusState(
|
||||
val displayedStatus: DisplayedStatus?,
|
||||
val rawStatus: UserStatus?,
|
||||
val pickerState: UserStatusPickerState,
|
||||
val updateStatusAction: AsyncAction<Unit>,
|
||||
val eventSink: (UserStatusEvent) -> Unit,
|
||||
)
|
||||
|
||||
|
||||
+3
@@ -9,6 +9,7 @@ package io.element.android.features.preferences.impl.userstatus
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||
|
||||
@@ -44,10 +45,12 @@ fun aUserStatusState(
|
||||
displayedStatus: DisplayedStatus? = null,
|
||||
rawStatus: UserStatus? = null,
|
||||
pickerState: UserStatusPickerState = UserStatusPickerState.Hidden,
|
||||
updateStatusAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (UserStatusEvent) -> Unit = {},
|
||||
) = UserStatusState(
|
||||
displayedStatus = displayedStatus,
|
||||
rawStatus = rawStatus,
|
||||
pickerState = pickerState,
|
||||
updateStatusAction = updateStatusAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
+39
@@ -8,9 +8,11 @@
|
||||
package io.element.android.features.preferences.impl.userstatus
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.emoji.test.fakeEmojiPickerPresenterFactory
|
||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -61,6 +63,24 @@ class UserStatusPresenterTest {
|
||||
val newState = awaitItem()
|
||||
assertThat(newState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||
assertThat(newState.displayedStatus).isEqualTo(DisplayedStatus.UserSet(status))
|
||||
assertThat(newState.updateStatusAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
assertThat(client.setUserStatusCalled).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Set event failure surfaces a Failure action`() = runTest {
|
||||
val client = FakeMatrixClient().apply {
|
||||
setUserStatusResult = Result.failure(AN_EXCEPTION)
|
||||
}
|
||||
createPresenter(client).test {
|
||||
val status = UserStatus(emoji = "💬", text = "In a meeting")
|
||||
awaitItem().eventSink(UserStatusEvent.SetStatus(status))
|
||||
val newState = awaitItem()
|
||||
assertThat(newState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||
// On failure the SDK does not emit a new profile, so nothing is displayed.
|
||||
assertThat(newState.displayedStatus).isNull()
|
||||
assertThat(newState.updateStatusAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
assertThat(client.setUserStatusCalled).isTrue()
|
||||
}
|
||||
}
|
||||
@@ -121,6 +141,25 @@ class UserStatusPresenterTest {
|
||||
val clearedState = awaitItem()
|
||||
assertThat(clearedState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||
assertThat(clearedState.displayedStatus).isNull()
|
||||
assertThat(clearedState.updateStatusAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
assertThat(client.clearUserStatusCalled).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Clear event failure surfaces a Failure action`() = runTest {
|
||||
val client = FakeMatrixClient().apply {
|
||||
clearUserStatusResult = Result.failure(AN_EXCEPTION)
|
||||
}
|
||||
createPresenter(client).test {
|
||||
val status = UserStatus(emoji = "☕", text = "Be right back")
|
||||
awaitItem().eventSink(UserStatusEvent.SetStatus(status))
|
||||
awaitItem().eventSink(UserStatusEvent.ClearStatus)
|
||||
val clearedState = awaitItem()
|
||||
assertThat(clearedState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||
// Clear failed, so the previously set status is still displayed.
|
||||
assertThat(clearedState.displayedStatus).isEqualTo(DisplayedStatus.UserSet(status))
|
||||
assertThat(clearedState.updateStatusAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
assertThat(client.clearUserStatusCalled).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user