Add preference screen for user profile

This commit is contained in:
Jorge Martín
2023-09-13 15:59:05 +02:00
parent fe41d2aa28
commit fa277f062f
13 changed files with 614 additions and 5 deletions

View File

@@ -44,10 +44,12 @@ dependencies {
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.mediapickers.api)
implementation(projects.libraries.mediaupload.api)
implementation(projects.features.rageshake.api)
implementation(projects.features.analytics.api)
implementation(projects.features.ftue.api)
implementation(projects.libraries.matrixui)
implementation(projects.features.logout.api)
implementation(projects.services.analytics.api)
implementation(projects.services.toolbox.api)

View File

@@ -38,6 +38,7 @@ import io.element.android.features.preferences.impl.developer.tracing.ConfigureT
import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode
import io.element.android.features.preferences.impl.root.PreferencesRootNode
import io.element.android.features.preferences.impl.user.screen.UserPreferencesNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
@@ -81,6 +82,9 @@ class PreferencesFlowNode @AssistedInject constructor(
@Parcelize
data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget
@Parcelize
data object UserProfile : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -114,6 +118,10 @@ class PreferencesFlowNode @AssistedInject constructor(
override fun onOpenAdvancedSettings() {
backstack.push(NavTarget.AdvancedSettings)
}
override fun onOpenUserProfile() {
backstack.push(NavTarget.UserProfile)
}
}
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
}
@@ -149,6 +157,9 @@ class PreferencesFlowNode @AssistedInject constructor(
NavTarget.AdvancedSettings -> {
createNode<AdvancedSettingsNode>(buildContext)
}
NavTarget.UserProfile -> {
createNode<UserPreferencesNode>(buildContext)
}
}
}

View File

@@ -47,6 +47,7 @@ class PreferencesRootNode @AssistedInject constructor(
fun onOpenDeveloperSettings()
fun onOpenNotificationSettings()
fun onOpenAdvancedSettings()
fun onOpenUserProfile()
}
private fun onOpenBugReport() {
@@ -91,6 +92,10 @@ class PreferencesRootNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onOpenNotificationSettings() }
}
private fun onOpenUserProfile() {
plugins<Callback>().forEach { it.onOpenUserProfile() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@@ -108,7 +113,8 @@ class PreferencesRootNode @AssistedInject constructor(
onOpenAdvancedSettings = this::onOpenAdvancedSettings,
onSuccessLogout = { onSuccessLogout(activity, it) },
onManageAccountClicked = { onManageAccountClicked(activity, it, isDark) },
onOpenNotificationSettings = this::onOpenNotificationSettings
onOpenNotificationSettings = this::onOpenNotificationSettings,
onOpenUserProfile = this::onOpenUserProfile,
)
}

View File

@@ -16,6 +16,7 @@
package io.element.android.features.preferences.impl.root
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
@@ -62,6 +63,7 @@ fun PreferencesRootView(
onOpenAdvancedSettings: () -> Unit,
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
onOpenNotificationSettings: () -> Unit,
onOpenUserProfile: () -> Unit,
modifier: Modifier = Modifier,
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@@ -73,7 +75,12 @@ fun PreferencesRootView(
title = stringResource(id = CommonStrings.common_settings),
snackbarHost = { SnackbarHost(snackbarHostState) }
) {
UserPreferences(state.myUser)
UserPreferences(
modifier = Modifier.clickable {
onOpenUserProfile()
},
user = state.myUser,
)
if (state.showCompleteVerification) {
PreferenceText(
title = stringResource(id = CommonStrings.action_complete_verification),
@@ -181,5 +188,6 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
onSuccessLogout = {},
onManageAccountClicked = {},
onOpenNotificationSettings = {},
onOpenUserProfile = {},
)
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.user.screen
import io.element.android.libraries.matrix.ui.media.AvatarAction
sealed interface UserPreferencesEvents {
data class HandleAvatarAction(val action: AvatarAction) : UserPreferencesEvents
data class UpdateDisplayName(val name: String) : UserPreferencesEvents
data object Save : UserPreferencesEvents
data object CancelSaveChanges : UserPreferencesEvents
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.user.screen
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class UserPreferencesNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: UserPreferencesPresenter,
) : Node(buildContext, plugins = plugins) {
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
UserPreferencesView(
state = state,
onBackPressed = ::navigateUp,
onProfileEdited = ::navigateUp, // TODO: check if something else is needed
modifier = modifier
)
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.user.screen
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.user.getCurrentUser
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class UserPreferencesPresenter @Inject constructor(
private val matrixClient: MatrixClient,
private val mediaPickerProvider: PickerProvider,
private val mediaPreProcessor: MediaPreProcessor,
) : Presenter<UserPreferencesState> {
@Composable
override fun present(): UserPreferencesState {
var currentUser by remember { mutableStateOf<MatrixUser?>(null) }
var userAvatarUri by rememberSaveable(currentUser) { mutableStateOf(currentUser?.avatarUrl?.let { Uri.parse(it) }) }
var userDisplayName by rememberSaveable(currentUser) { mutableStateOf(currentUser?.displayName) }
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
onResult = { uri -> if (uri != null) userAvatarUri = uri }
)
val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker(
onResult = { uri -> if (uri != null) userAvatarUri = uri }
)
LaunchedEffect(Unit) {
currentUser = matrixClient.getCurrentUser()
}
val avatarActions by remember(userAvatarUri) {
derivedStateOf {
listOfNotNull(
AvatarAction.TakePhoto,
AvatarAction.ChoosePhoto,
AvatarAction.Remove.takeIf { userAvatarUri != null },
).toImmutableList()
}
}
val saveAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
val localCoroutineScope = rememberCoroutineScope()
fun handleEvents(event: UserPreferencesEvents) {
when (event) {
is UserPreferencesEvents.Save -> currentUser?.let {
localCoroutineScope.saveChanges(userDisplayName, userAvatarUri, it, saveAction)
}
is UserPreferencesEvents.HandleAvatarAction -> {
when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
AvatarAction.TakePhoto -> cameraPhotoPicker.launch()
AvatarAction.Remove -> userAvatarUri = null
}
}
is UserPreferencesEvents.UpdateDisplayName -> userDisplayName = event.name
UserPreferencesEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized
}
}
val canSave = remember(userDisplayName, userAvatarUri, currentUser) {
val hasProfileChanged = hasDisplayNameChanged(userDisplayName, currentUser)
|| hasAvatarUrlChanged(userAvatarUri, currentUser)
!userDisplayName.isNullOrBlank() && hasProfileChanged
}
return UserPreferencesState(
userId = currentUser?.userId,
displayName = userDisplayName.orEmpty(),
userAvatarUrl = userAvatarUri,
avatarActions = avatarActions,
saveButtonEnabled = canSave && saveAction.value !is Async.Loading,
saveAction = saveAction.value,
eventSink = ::handleEvents
)
}
private fun hasDisplayNameChanged(name: String?, currentUser: MatrixUser?) = name?.trim() != currentUser?.displayName?.trim()
private fun hasAvatarUrlChanged(avatarUri: Uri?, currentUser: MatrixUser?) = avatarUri?.toString()?.trim() != currentUser?.avatarUrl?.trim()
private fun CoroutineScope.saveChanges(name: String?, avatarUri: Uri?, currentUser: MatrixUser, action: MutableState<Async<Unit>>) = launch {
matrixClient.getCurrentUser()
val results = mutableListOf<Result<Unit>>()
suspend {
if (!name.isNullOrEmpty() && name.trim() != currentUser.displayName.orEmpty().trim()) {
results.add(matrixClient.setDisplayName(name).onFailure {
Timber.e(it, "Failed to set user's display name")
})
}
if (avatarUri?.toString()?.trim() != currentUser.avatarUrl?.trim()) {
results.add(updateAvatar(avatarUri).onFailure {
Timber.e(it, "Failed to update user's avatar")
})
}
if (results.all { it.isSuccess }) Unit else results.first { it.isFailure }.getOrThrow()
}.runCatchingUpdatingState(action)
}
private suspend fun updateAvatar(avatarUri: Uri?): Result<Unit> {
return runCatching {
if (avatarUri != null) {
val preprocessed = mediaPreProcessor.process(avatarUri, MimeTypes.Jpeg, compressIfPossible = false).getOrThrow()
matrixClient.uploadAvatar(MimeTypes.Jpeg, preprocessed.file.readBytes()).getOrThrow()
} else {
matrixClient.removeAvatar().getOrThrow()
}
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.user.screen
import android.net.Uri
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.media.AvatarAction
import kotlinx.collections.immutable.ImmutableList
data class UserPreferencesState(
val userId: UserId?,
val displayName: String,
val userAvatarUrl: Uri?,
val avatarActions: ImmutableList<AvatarAction>,
val saveButtonEnabled: Boolean,
val saveAction: Async<Unit>,
val eventSink: (UserPreferencesEvents) -> Unit
)

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.user.screen
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.persistentListOf
open class UserPreferencesStateProvider : PreviewParameterProvider<UserPreferencesState> {
override val values: Sequence<UserPreferencesState>
get() = sequenceOf(
aUserPreferencesState(),
// Add other states here
)
}
fun aUserPreferencesState() = UserPreferencesState(
userId = UserId("@john.doe:matrix.org"),
displayName = "John Doe",
userAvatarUrl = null,
avatarActions = persistentListOf(),
saveAction = Async.Uninitialized,
saveButtonEnabled = true,
eventSink = {}
)

View File

@@ -0,0 +1,271 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.user.screen
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddAPhoto
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.LabelledTextField
import io.element.android.libraries.designsystem.components.ProgressDialog
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.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Scaffold
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.TopAppBar
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable
fun UserPreferencesView(
state: UserPreferencesState,
onBackPressed: () -> Unit,
onProfileEdited: () -> Unit,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
val itemActionsBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
)
fun onAvatarClicked() {
focusManager.clearFocus()
coroutineScope.launch {
itemActionsBottomSheetState.show()
}
}
Scaffold(
modifier = modifier.clearFocusOnTap(focusManager),
topBar = {
TopAppBar(
title = {
Text(
text = "Edit profile",
style = ElementTheme.typography.aliasScreenTitle,
)
},
navigationIcon = { BackButton(onClick = onBackPressed) },
actions = {
TextButton(
text = stringResource(CommonStrings.action_save),
enabled = state.saveButtonEnabled,
onClick = {
focusManager.clearFocus()
state.eventSink(UserPreferencesEvents.Save)
},
)
}
)
},
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.padding(horizontal = 16.dp)
.navigationBarsPadding()
.imePadding()
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(24.dp))
EditableAvatarView(state, ::onAvatarClicked)
Spacer(modifier = Modifier.height(60.dp))
LabelledTextField(
label = "Display name",
value = state.displayName,
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
singleLine = true,
onValueChange = { state.eventSink(UserPreferencesEvents.UpdateDisplayName(it)) },
)
}
AvatarActionBottomSheet(
actions = state.avatarActions,
modalBottomSheetState = itemActionsBottomSheetState,
onActionSelected = { state.eventSink(UserPreferencesEvents.HandleAvatarAction(it)) }
)
when (state.saveAction) {
is Async.Loading -> {
ProgressDialog(text = "Updating profile...")
}
is Async.Failure -> {
ErrorDialog(
content = "Error updating profile",
onDismiss = { state.eventSink(UserPreferencesEvents.CancelSaveChanges) },
)
}
is Async.Success -> {
LaunchedEffect(state.saveAction) {
onProfileEdited()
}
}
else -> Unit
}
}
}
@Composable
private fun EditableAvatarView(
state: UserPreferencesState,
onAvatarClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Box(
modifier = Modifier
.size(70.dp)
.clickable(onClick = onAvatarClicked)
) {
// TODO this might be able to be simplified into a single component once send/receive media is done
when (state.userAvatarUrl?.scheme) {
null, "mxc" -> {
Avatar(
avatarData = AvatarData(state.userId.toString(), state.displayName, state.userAvatarUrl?.toString(), size = AvatarSize.RoomHeader),
modifier = Modifier.fillMaxSize(),
)
}
else -> {
UnsavedAvatar(
avatarUri = state.userAvatarUrl,
modifier = Modifier.fillMaxSize(),
)
}
}
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
.size(24.dp),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Outlined.AddAPhoto,
contentDescription = "",
tint = MaterialTheme.colorScheme.onPrimary,
)
}
}
}
}
@Composable
private fun LabelledReadOnlyField(
title: String,
value: String,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.primary,
text = title,
)
Text(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary,
text = value,
)
}
}
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
pointerInput(Unit) {
detectTapGestures(onTap = {
focusManager.clearFocus()
})
}
@Preview
@Composable
fun UserPreferencesViewLightPreview(@PreviewParameter(UserPreferencesStateProvider::class) state: UserPreferencesState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
fun UserPreferencesViewDarkPreview(@PreviewParameter(UserPreferencesStateProvider::class) state: UserPreferencesState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: UserPreferencesState) {
UserPreferencesView(
onBackPressed = {},
onProfileEdited = {},
state = state,
)
}

View File

@@ -47,6 +47,9 @@ interface MatrixClient : Closeable {
suspend fun createDM(userId: UserId): Result<RoomId>
suspend fun getProfile(userId: UserId): Result<MatrixUser>
suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults>
suspend fun setDisplayName(displayName: String): Result<Unit>
suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit>
suspend fun removeAvatar(): Result<Unit>
fun syncService(): SyncService
fun sessionVerificationService(): SessionVerificationService
fun pushersService(): PushersService

View File

@@ -29,8 +29,8 @@ anvil {
}
dependencies {
// implementation(projects.libraries.rustsdk)
implementation(libs.matrix.sdk)
implementation(projects.libraries.rustsdk)
// implementation(libs.matrix.sdk)
implementation(projects.libraries.di)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.network)

View File

@@ -276,6 +276,23 @@ class RustMatrixClient constructor(
}
}
override suspend fun setDisplayName(displayName: String): Result<Unit> =
withContext(sessionDispatcher) {
runCatching { client.setDisplayName(displayName) }
}
@OptIn(ExperimentalUnsignedTypes::class)
override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit> =
withContext(sessionDispatcher) {
runCatching { client.uploadAvatar(mimeType, data.toUByteArray().toList()) }
}
override suspend fun removeAvatar(): Result<Unit> =
withContext(sessionDispatcher) {
runCatching { client.removeAvatar() }
}
override fun syncService(): SyncService = rustSyncService
override fun sessionVerificationService(): SessionVerificationService = verificationService