mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 06:55:41 +00:00
Wire the emoji picker into the user-status feature
This commit is contained in:
Generated
+34
@@ -1,5 +1,39 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
|
<JavaCodeStyleSettings>
|
||||||
|
<option name="IMPORT_LAYOUT_TABLE">
|
||||||
|
<value>
|
||||||
|
<package name="android" withSubpackages="true" static="true" />
|
||||||
|
<package name="androidx" withSubpackages="true" static="true" />
|
||||||
|
<package name="com" withSubpackages="true" static="true" />
|
||||||
|
<package name="junit" withSubpackages="true" static="true" />
|
||||||
|
<package name="net" withSubpackages="true" static="true" />
|
||||||
|
<package name="org" withSubpackages="true" static="true" />
|
||||||
|
<package name="java" withSubpackages="true" static="true" />
|
||||||
|
<package name="javax" withSubpackages="true" static="true" />
|
||||||
|
<package name="" withSubpackages="true" static="true" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="android" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="androidx" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="com" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="junit" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="net" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="org" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="java" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="javax" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</JavaCodeStyleSettings>
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
|
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ dependencies {
|
|||||||
implementation(projects.features.deactivation.api)
|
implementation(projects.features.deactivation.api)
|
||||||
implementation(projects.features.home.api)
|
implementation(projects.features.home.api)
|
||||||
implementation(projects.features.invite.api)
|
implementation(projects.features.invite.api)
|
||||||
|
implementation(projects.libraries.emoji.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.services.analytics.compose)
|
implementation(projects.services.analytics.compose)
|
||||||
implementation(projects.services.appnavstate.api)
|
implementation(projects.services.appnavstate.api)
|
||||||
@@ -98,6 +99,7 @@ dependencies {
|
|||||||
|
|
||||||
testCommonDependencies(libs, true)
|
testCommonDependencies(libs, true)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.emoji.test)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.libraries.mediapickers.test)
|
testImplementation(projects.libraries.mediapickers.test)
|
||||||
testImplementation(projects.libraries.mediaupload.test)
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
|
|||||||
+3
@@ -24,6 +24,7 @@ import io.element.android.features.logout.api.direct.DirectLogoutView
|
|||||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||||
import io.element.android.libraries.architecture.callback
|
import io.element.android.libraries.architecture.callback
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
@@ -33,6 +34,7 @@ class PreferencesRootNode(
|
|||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: PreferencesRootPresenter,
|
private val presenter: PreferencesRootPresenter,
|
||||||
private val directLogoutView: DirectLogoutView,
|
private val directLogoutView: DirectLogoutView,
|
||||||
|
private val emojiPickerRenderer: EmojiPickerRenderer,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun navigateToAddAccount()
|
fun navigateToAddAccount()
|
||||||
@@ -75,6 +77,7 @@ class PreferencesRootNode(
|
|||||||
val isDark = ElementTheme.isLightTheme.not()
|
val isDark = ElementTheme.isLightTheme.not()
|
||||||
PreferencesRootView(
|
PreferencesRootView(
|
||||||
state = state,
|
state = state,
|
||||||
|
emojiPickerRenderer = emojiPickerRenderer,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackClick = this::navigateUp,
|
onBackClick = this::navigateUp,
|
||||||
onAddAccountClick = callback::navigateToAddAccount,
|
onAddAccountClick = callback::navigateToAddAccount,
|
||||||
|
|||||||
+11
-1
@@ -41,6 +41,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
|
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
||||||
|
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
|
||||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
@@ -49,6 +51,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||||||
@Composable
|
@Composable
|
||||||
fun PreferencesRootView(
|
fun PreferencesRootView(
|
||||||
state: PreferencesRootState,
|
state: PreferencesRootState,
|
||||||
|
emojiPickerRenderer: EmojiPickerRenderer,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onAddAccountClick: () -> Unit,
|
onAddAccountClick: () -> Unit,
|
||||||
onSecureBackupClick: () -> Unit,
|
onSecureBackupClick: () -> Unit,
|
||||||
@@ -90,7 +93,11 @@ fun PreferencesRootView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.userStatusState != null) {
|
if (state.userStatusState != null) {
|
||||||
UserStatusSection(state.userStatusState, showTopDivider = !state.isMultiAccountEnabled)
|
UserStatusSection(
|
||||||
|
userStatusState = state.userStatusState,
|
||||||
|
emojiPickerRenderer = emojiPickerRenderer,
|
||||||
|
showTopDivider = !state.isMultiAccountEnabled,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// 'Account' section
|
// 'Account' section
|
||||||
ManageAccountSection(
|
ManageAccountSection(
|
||||||
@@ -135,6 +142,7 @@ fun PreferencesRootView(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.UserStatusSection(
|
private fun ColumnScope.UserStatusSection(
|
||||||
userStatusState: UserStatusState,
|
userStatusState: UserStatusState,
|
||||||
|
emojiPickerRenderer: EmojiPickerRenderer,
|
||||||
showTopDivider: Boolean,
|
showTopDivider: Boolean,
|
||||||
) {
|
) {
|
||||||
if (showTopDivider) {
|
if (showTopDivider) {
|
||||||
@@ -145,6 +153,7 @@ private fun ColumnScope.UserStatusSection(
|
|||||||
}
|
}
|
||||||
UserStatusView(
|
UserStatusView(
|
||||||
state = userStatusState,
|
state = userStatusState,
|
||||||
|
emojiPickerRenderer = emojiPickerRenderer,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
@@ -369,6 +378,7 @@ internal fun PreferencesRootViewDarkPreview(@PreviewParameter(PreferencesRootSta
|
|||||||
private fun ContentToPreview(state: PreferencesRootState) {
|
private fun ContentToPreview(state: PreferencesRootState) {
|
||||||
PreferencesRootView(
|
PreferencesRootView(
|
||||||
state = state,
|
state = state,
|
||||||
|
emojiPickerRenderer = NoOpEmojiPickerRenderer,
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
onAddAccountClick = {},
|
onAddAccountClick = {},
|
||||||
onOpenAnalytics = {},
|
onOpenAnalytics = {},
|
||||||
|
|||||||
+2
@@ -16,5 +16,7 @@ sealed interface UserStatusEvent {
|
|||||||
data object OpenCustomInput : UserStatusEvent
|
data object OpenCustomInput : UserStatusEvent
|
||||||
data object CancelCustomInput : UserStatusEvent
|
data object CancelCustomInput : UserStatusEvent
|
||||||
data class UpdateCustomEmoji(val emoji: String) : UserStatusEvent
|
data class UpdateCustomEmoji(val emoji: String) : UserStatusEvent
|
||||||
|
data object OpenEmojiPicker : UserStatusEvent
|
||||||
|
data object DismissEmojiPicker : UserStatusEvent
|
||||||
data object ClearStatus : UserStatusEvent
|
data object ClearStatus : UserStatusEvent
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-14
@@ -19,36 +19,32 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
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
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class UserStatusPresenter(
|
class UserStatusPresenter(
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
|
emojiPickerPresenterFactory: EmojiPickerPresenter.Factory,
|
||||||
) : Presenter<UserStatusState> {
|
) : Presenter<UserStatusState> {
|
||||||
|
private val emojiPickerPresenter = emojiPickerPresenterFactory.create(EmptyGetRecentEmojis)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): UserStatusState {
|
override fun present(): UserStatusState {
|
||||||
val userProfile by matrixClient.userProfile.collectAsState()
|
val userProfile by matrixClient.userProfile.collectAsState()
|
||||||
var pickerState by remember { mutableStateOf<UserStatusPickerState>(UserStatusPickerState.Hidden) }
|
var pickerState by remember { mutableStateOf<UserStatusPickerState>(UserStatusPickerState.Hidden) }
|
||||||
|
var isEmojiPickerVisible by remember { mutableStateOf(false) }
|
||||||
val customTextFieldState = rememberTextFieldState()
|
val customTextFieldState = rememberTextFieldState()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
fun handleEvent(event: UserStatusEvent) {
|
fun handleEvent(event: UserStatusEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
// Mode transitions
|
||||||
UserStatusEvent.OpenPicker -> pickerState = UserStatusPickerState.ShowingPicker
|
UserStatusEvent.OpenPicker -> pickerState = UserStatusPickerState.ShowingPicker
|
||||||
UserStatusEvent.DismissPicker -> pickerState = UserStatusPickerState.Hidden
|
UserStatusEvent.DismissPicker -> pickerState = UserStatusPickerState.Hidden
|
||||||
UserStatusEvent.OpenCustomInput -> {
|
UserStatusEvent.CancelCustomInput -> pickerState = UserStatusPickerState.Hidden
|
||||||
val raw = userProfile.rawStatus
|
|
||||||
if (raw != null) {
|
|
||||||
customTextFieldState.setTextAndPlaceCursorAtEnd(raw.text)
|
|
||||||
} else {
|
|
||||||
customTextFieldState.clearText()
|
|
||||||
}
|
|
||||||
pickerState = UserStatusPickerState.CustomInput(
|
|
||||||
emoji = raw?.emoji ?: "😀",
|
|
||||||
textFieldState = customTextFieldState,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is UserStatusEvent.SetStatus -> {
|
is UserStatusEvent.SetStatus -> {
|
||||||
pickerState = UserStatusPickerState.Hidden
|
pickerState = UserStatusPickerState.Hidden
|
||||||
coroutineScope.launch { matrixClient.setUserStatus(event.status) }
|
coroutineScope.launch { matrixClient.setUserStatus(event.status) }
|
||||||
@@ -57,19 +53,56 @@ class UserStatusPresenter(
|
|||||||
pickerState = UserStatusPickerState.Hidden
|
pickerState = UserStatusPickerState.Hidden
|
||||||
coroutineScope.launch { matrixClient.clearUserStatus() }
|
coroutineScope.launch { matrixClient.clearUserStatus() }
|
||||||
}
|
}
|
||||||
UserStatusEvent.CancelCustomInput -> pickerState = UserStatusPickerState.Hidden
|
UserStatusEvent.OpenCustomInput -> {
|
||||||
|
val raw = userProfile.rawStatus
|
||||||
|
if (raw != null) {
|
||||||
|
customTextFieldState.setTextAndPlaceCursorAtEnd(raw.text)
|
||||||
|
} else {
|
||||||
|
customTextFieldState.clearText()
|
||||||
|
}
|
||||||
|
isEmojiPickerVisible = false
|
||||||
|
pickerState = UserStatusPickerState.CustomInput(
|
||||||
|
emoji = raw?.emoji ?: "😀",
|
||||||
|
textFieldState = customTextFieldState,
|
||||||
|
emojiPickerSheetState = EmojiPickerSheetState.Hidden,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Custom-input mutations
|
||||||
is UserStatusEvent.UpdateCustomEmoji -> {
|
is UserStatusEvent.UpdateCustomEmoji -> {
|
||||||
|
isEmojiPickerVisible = false
|
||||||
val current = pickerState as? UserStatusPickerState.CustomInput ?: return
|
val current = pickerState as? UserStatusPickerState.CustomInput ?: return
|
||||||
pickerState = current.copy(emoji = event.emoji)
|
pickerState = current.copy(emoji = event.emoji)
|
||||||
}
|
}
|
||||||
|
// Emoji-picker visibility (only meaningful within CustomInput mode)
|
||||||
|
UserStatusEvent.OpenEmojiPicker -> isEmojiPickerVisible = true
|
||||||
|
UserStatusEvent.DismissEmojiPicker -> isEmojiPickerVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val effectivePickerState = when (val current = pickerState) {
|
||||||
|
is UserStatusPickerState.CustomInput -> {
|
||||||
|
val emojiPickerState = presentEmojiPickerSheetState(isEmojiPickerVisible)
|
||||||
|
current.copy(emojiPickerSheetState = emojiPickerState)
|
||||||
|
}
|
||||||
|
else -> current
|
||||||
|
}
|
||||||
|
|
||||||
return UserStatusState(
|
return UserStatusState(
|
||||||
displayedStatus = userProfile.displayedStatus,
|
displayedStatus = userProfile.displayedStatus,
|
||||||
rawStatus = userProfile.rawStatus,
|
rawStatus = userProfile.rawStatus,
|
||||||
pickerState = pickerState,
|
pickerState = effectivePickerState,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun presentEmojiPickerSheetState(isVisible: Boolean): EmojiPickerSheetState {
|
||||||
|
if (!isVisible) return EmojiPickerSheetState.Hidden
|
||||||
|
val emojiPickerState = emojiPickerPresenter.present()
|
||||||
|
return if (emojiPickerState.isReady) {
|
||||||
|
EmojiPickerSheetState.Shown(emojiPickerState)
|
||||||
|
} else {
|
||||||
|
EmojiPickerSheetState.Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -11,6 +11,7 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.compose.foundation.text.input.TextFieldState
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
|
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.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.api.user.UserStatus
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
|
|
||||||
@@ -28,9 +29,17 @@ sealed interface UserStatusPickerState {
|
|||||||
data class CustomInput(
|
data class CustomInput(
|
||||||
val emoji: String,
|
val emoji: String,
|
||||||
val textFieldState: TextFieldState,
|
val textFieldState: TextFieldState,
|
||||||
|
val emojiPickerSheetState: EmojiPickerSheetState,
|
||||||
) : UserStatusPickerState
|
) : UserStatusPickerState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface EmojiPickerSheetState {
|
||||||
|
data object Hidden : EmojiPickerSheetState
|
||||||
|
data object Loading : EmojiPickerSheetState
|
||||||
|
data class Shown(val state: EmojiPickerState) : EmojiPickerSheetState
|
||||||
|
}
|
||||||
|
|
||||||
enum class PredefinedUserStatus(val emoji: String, @StringRes val labelRes: Int) {
|
enum class PredefinedUserStatus(val emoji: String, @StringRes val labelRes: Int) {
|
||||||
IN_A_MEETING("💬", R.string.common_user_status_in_a_meeting),
|
IN_A_MEETING("💬", R.string.common_user_status_in_a_meeting),
|
||||||
FOCUS_TIME("💡", R.string.common_user_status_focus_time),
|
FOCUS_TIME("💡", R.string.common_user_status_focus_time),
|
||||||
|
|||||||
+14
-2
@@ -23,8 +23,20 @@ internal class UserStatusStateProvider : PreviewParameterProvider<UserStatusStat
|
|||||||
rawStatus = UserStatus("🌴", "Away"),
|
rawStatus = UserStatus("🌴", "Away"),
|
||||||
pickerState = UserStatusPickerState.ShowingPicker,
|
pickerState = UserStatusPickerState.ShowingPicker,
|
||||||
),
|
),
|
||||||
aUserStatusState(pickerState = UserStatusPickerState.CustomInput(emoji = "😀", textFieldState = TextFieldState())),
|
aUserStatusState(
|
||||||
aUserStatusState(pickerState = UserStatusPickerState.CustomInput(emoji = "🚀", textFieldState = TextFieldState("Working on something"))),
|
pickerState = UserStatusPickerState.CustomInput(
|
||||||
|
emoji = "😀",
|
||||||
|
textFieldState = TextFieldState(),
|
||||||
|
emojiPickerSheetState = EmojiPickerSheetState.Hidden
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aUserStatusState(
|
||||||
|
pickerState = UserStatusPickerState.CustomInput(
|
||||||
|
emoji = "🚀",
|
||||||
|
textFieldState = TextFieldState("Working on something"),
|
||||||
|
emojiPickerSheetState = EmojiPickerSheetState.Hidden
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+49
-2
@@ -10,6 +10,7 @@ package io.element.android.features.preferences.impl.userstatus
|
|||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -58,13 +59,17 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||||
import io.element.android.libraries.designsystem.theme.components.hide
|
import io.element.android.libraries.designsystem.theme.components.hide
|
||||||
|
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
||||||
|
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
|
||||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.api.user.UserStatus
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserStatusView(
|
fun UserStatusView(
|
||||||
state: UserStatusState,
|
state: UserStatusState,
|
||||||
|
emojiPickerRenderer: EmojiPickerRenderer,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (val pickerState = state.pickerState) {
|
when (val pickerState = state.pickerState) {
|
||||||
@@ -103,8 +108,18 @@ fun UserStatusView(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onCancel = { state.eventSink(UserStatusEvent.CancelCustomInput) },
|
onCancel = { state.eventSink(UserStatusEvent.CancelCustomInput) },
|
||||||
|
onEmojiClick = { state.eventSink(UserStatusEvent.OpenEmojiPicker) },
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
when (val sheet = pickerState.emojiPickerSheetState) {
|
||||||
|
EmojiPickerSheetState.Hidden, EmojiPickerSheetState.Loading -> Unit
|
||||||
|
is EmojiPickerSheetState.Shown -> EmojiPickerBottomSheet(
|
||||||
|
pickerState = sheet.state,
|
||||||
|
emojiPickerRenderer = emojiPickerRenderer,
|
||||||
|
onSelectEmoji = { emoji -> state.eventSink(UserStatusEvent.UpdateCustomEmoji(emoji.unicode)) },
|
||||||
|
onDismiss = { state.eventSink(UserStatusEvent.DismissEmojiPicker) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,6 +246,7 @@ private fun CustomStatusInputRow(
|
|||||||
rawStatus: UserStatus?,
|
rawStatus: UserStatus?,
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
|
onEmojiClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val hasText by remember { derivedStateOf { textFieldState.text.isNotEmpty() } }
|
val hasText by remember { derivedStateOf { textFieldState.text.isNotEmpty() } }
|
||||||
@@ -297,7 +313,7 @@ private fun CustomStatusInputRow(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(50.dp)
|
.size(50.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.clickable { },
|
.clickable(onClick = onEmojiClick),
|
||||||
) {
|
) {
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
EmojiText(emoji)
|
EmojiText(emoji)
|
||||||
@@ -308,8 +324,39 @@ private fun CustomStatusInputRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun EmojiPickerBottomSheet(
|
||||||
|
pickerState: io.element.android.libraries.emoji.api.picker.EmojiPickerState,
|
||||||
|
emojiPickerRenderer: EmojiPickerRenderer,
|
||||||
|
onSelectEmoji: (io.element.android.emojibasebindings.Emoji) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden)
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = sheetState,
|
||||||
|
scrollable = false,
|
||||||
|
) {
|
||||||
|
emojiPickerRenderer.Render(
|
||||||
|
state = pickerState,
|
||||||
|
onSelectEmoji = { emoji ->
|
||||||
|
sheetState.hide(coroutineScope) {
|
||||||
|
onSelectEmoji(emoji)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedEmojis = persistentSetOf(),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun UserStatusViewPreview(@PreviewParameter(UserStatusStateProvider::class) state: UserStatusState) = ElementPreview {
|
internal fun UserStatusViewPreview(@PreviewParameter(UserStatusStateProvider::class) state: UserStatusState) = ElementPreview {
|
||||||
UserStatusView(state = state)
|
UserStatusView(
|
||||||
|
state = state,
|
||||||
|
emojiPickerRenderer = NoOpEmojiPickerRenderer,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -19,6 +19,7 @@ import androidx.compose.ui.test.performClick
|
|||||||
import androidx.compose.ui.test.performScrollTo
|
import androidx.compose.ui.test.performScrollTo
|
||||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
|
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
@@ -466,6 +467,7 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
|
|||||||
setContent {
|
setContent {
|
||||||
PreferencesRootView(
|
PreferencesRootView(
|
||||||
state = state,
|
state = state,
|
||||||
|
emojiPickerRenderer = NoOpEmojiPickerRenderer,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
onAddAccountClick = onAddAccountClick,
|
onAddAccountClick = onAddAccountClick,
|
||||||
onSecureBackupClick = onSecureBackupClick,
|
onSecureBackupClick = onSecureBackupClick,
|
||||||
|
|||||||
+5
-1
@@ -8,6 +8,7 @@
|
|||||||
package io.element.android.features.preferences.impl.userstatus
|
package io.element.android.features.preferences.impl.userstatus
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
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.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.api.user.UserStatus
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
@@ -17,7 +18,10 @@ import org.junit.Test
|
|||||||
|
|
||||||
class UserStatusPresenterTest {
|
class UserStatusPresenterTest {
|
||||||
private fun createPresenter(client: FakeMatrixClient = FakeMatrixClient()) =
|
private fun createPresenter(client: FakeMatrixClient = FakeMatrixClient()) =
|
||||||
UserStatusPresenter(matrixClient = client)
|
UserStatusPresenter(
|
||||||
|
matrixClient = client,
|
||||||
|
emojiPickerPresenterFactory = fakeEmojiPickerPresenterFactory(),
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state - no status set`() = runTest {
|
fun `initial state - no status set`() = runTest {
|
||||||
|
|||||||
+11
@@ -31,3 +31,14 @@ interface EmojiPickerRenderer {
|
|||||||
contentDescription: @Composable (Emoji, Boolean) -> String = { emoji, _ -> emoji.unicode },
|
contentDescription: @Composable (Emoji, Boolean) -> String = { emoji, _ -> emoji.unicode },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object NoOpEmojiPickerRenderer : EmojiPickerRenderer {
|
||||||
|
@Composable
|
||||||
|
override fun Render(
|
||||||
|
state: EmojiPickerState,
|
||||||
|
onSelectEmoji: (Emoji) -> Unit,
|
||||||
|
selectedEmojis: ImmutableSet<String>,
|
||||||
|
modifier: Modifier,
|
||||||
|
contentDescription: @Composable (Emoji, Boolean) -> String,
|
||||||
|
) = Unit
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user