Merge branch 'develop' of https://github.com/vector-im/element-x-android into dla/feature/custom_room_notification_settings_list

This commit is contained in:
David Langley 2023-10-18 22:07:14 +01:00
commit 87b8bfe99d
1981 changed files with 15504 additions and 5063 deletions

View file

@ -21,8 +21,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.ui.strings.CommonStrings
@ -33,7 +33,7 @@ fun AboutView(
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferenceView(
PreferencePage(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.common_about)
@ -47,7 +47,7 @@ fun AboutView(
}
}
@DayNightPreviews
@PreviewsDayNight
@Composable
internal fun AboutViewPreview(@PreviewParameter(AboutStateProvider::class) state: AboutState) = ElementPreview {
AboutView(

View file

@ -20,10 +20,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
@ -32,30 +33,27 @@ fun AdvancedSettingsView(
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferenceView(
PreferencePage(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.common_advanced_settings)
) {
PreferenceSwitch(
title = stringResource(id = CommonStrings.common_rich_text_editor),
// TODO i18n
subtitle = "Disable the rich text editor to type Markdown manually",
subtitle = stringResource(id = R.string.screen_advanced_settings_rich_text_editor_description),
isChecked = state.isRichTextEditorEnabled,
onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetRichTextEditorEnabled(it)) },
)
PreferenceSwitch(
// TODO i18n
title = "Developer mode",
// TODO i18n
subtitle = "The developer mode activates hidden features. For developers only!",
title = stringResource(id = R.string.screen_advanced_settings_developer_mode),
subtitle = stringResource(id = R.string.screen_advanced_settings_developer_mode_description),
isChecked = state.isDeveloperModeEnabled,
onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) },
)
}
}
@DayNightPreviews
@PreviewsDayNight
@Composable
internal fun AdvancedSettingsViewPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) =
ElementPreview {

View file

@ -21,8 +21,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesView
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.ui.strings.CommonStrings
@ -32,7 +32,7 @@ fun AnalyticsSettingsView(
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferenceView(
PreferencePage(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.common_analytics)
@ -43,7 +43,7 @@ fun AnalyticsSettingsView(
}
}
@DayNightPreviews
@PreviewsDayNight
@Composable
internal fun AnalyticsSettingsViewPreview(@PreviewParameter(AnalyticsSettingsStateProvider::class) state: AnalyticsSettingsState) = ElementPreview {
AnalyticsSettingsView(

View file

@ -23,8 +23,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.featureflag.ui.FeatureListView
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
@ -38,7 +38,7 @@ fun DeveloperSettingsView(
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferenceView(
PreferencePage(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.common_developer_options)
@ -85,7 +85,7 @@ fun DeveloperSettingsView(
}
@Composable
fun FeatureListContent(
private fun FeatureListContent(
state: DeveloperSettingsState,
modifier: Modifier = Modifier
) {
@ -100,7 +100,7 @@ fun FeatureListContent(
)
}
@DayNightPreviews
@PreviewsDayNight
@Composable
internal fun DeveloperSettingsViewPreview(@PreviewParameter(DeveloperSettingsStateProvider::class) state: DeveloperSettingsState) = ElementPreview {
DeveloperSettingsView(

View file

@ -28,11 +28,6 @@ import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.ArrowDropUp
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -40,12 +35,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
@ -56,6 +52,7 @@ import io.element.android.libraries.designsystem.theme.components.ListItem
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.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.matrix.api.tracing.Target
import io.element.android.libraries.theme.ElementTheme
@ -93,7 +90,7 @@ fun ConfigureTracingView(
onClick = { showMenu = !showMenu }
) {
Icon(
imageVector = Icons.Default.MoreVert,
resourceId = CommonDrawables.ic_compound_overflow_vertical,
tint = ElementTheme.materialColors.secondary,
contentDescription = null,
)
@ -110,7 +107,7 @@ fun ConfigureTracingView(
text = { Text("Reset to default") },
leadingIcon = {
Icon(
Icons.Outlined.Delete,
resourceId = CommonDrawables.ic_compound_delete,
tint = ElementTheme.materialColors.secondary,
contentDescription = null,
)
@ -142,7 +139,7 @@ fun ConfigureTracingView(
}
@Composable
fun CrateListContent(
private fun CrateListContent(
state: ConfigureTracingState,
modifier: Modifier = Modifier
) {
@ -181,7 +178,7 @@ private fun TargetAndLogLevelListView(
}
@Composable
fun TargetAndLogLevelView(
private fun TargetAndLogLevelView(
target: Target,
logLevel: LogLevel,
onLogLevelChange: (LogLevel) -> Unit,
@ -200,7 +197,7 @@ fun TargetAndLogLevelView(
}
@Composable
fun LogLevelDropdownMenu(
private fun LogLevelDropdownMenu(
logLevel: LogLevel,
onLogLevelChange: (LogLevel) -> Unit,
modifier: Modifier = Modifier,
@ -212,11 +209,11 @@ fun LogLevelDropdownMenu(
text = { Text(text = logLevel.filter) },
onClick = { expanded = !expanded },
trailingIcon = {
if (expanded) {
Icon(Icons.Default.ArrowDropUp, contentDescription = null)
} else {
Icon(Icons.Default.ArrowDropDown, contentDescription = null)
}
Icon(
modifier = Modifier.rotate(if (expanded) 180f else 0f),
resourceId = CommonDrawables.ic_compound_chevron_down,
contentDescription = null,
)
},
)
DropdownMenu(
@ -238,7 +235,7 @@ fun LogLevelDropdownMenu(
}
}
@DayNightPreviews
@PreviewsDayNight
@Composable
internal fun ConfigureTracingViewPreview(
@PreviewParameter(ConfigureTracingStateProvider::class) state: ConfigureTracingState

View file

@ -23,8 +23,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NotificationsOff
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -42,13 +40,14 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.theme.ElementTheme
@ -70,7 +69,7 @@ fun NotificationSettingsView(
else -> Unit
}
}
PreferenceView(
PreferencePage(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.screen_notification_settings_title)
@ -82,7 +81,7 @@ fun NotificationSettingsView(
onContinueClicked = { state.eventSink(NotificationSettingsEvents.FixConfigurationMismatch) },
onDismissError = { state.eventSink(NotificationSettingsEvents.ClearConfigurationMismatchError) },
)
NotificationSettingsState.MatrixSettings.Uninitialized -> return@PreferenceView
NotificationSettingsState.MatrixSettings.Uninitialized -> return@PreferencePage
is NotificationSettingsState.MatrixSettings.Valid -> NotificationSettingsContentView(
matrixSettings = state.matrixSettings,
systemSettings = state.appSettings,
@ -123,7 +122,7 @@ private fun NotificationSettingsContentView(
val context = LocalContext.current
if (systemSettings.appNotificationsEnabled && !systemSettings.systemNotificationsEnabled) {
PreferenceText(
icon = Icons.Filled.NotificationsOff,
iconResourceId = CommonDrawables.ic_compound_notifications_solid_off,
title = stringResource(id = CommonStrings.screen_notification_settings_system_notifications_turned_off),
subtitle = stringResource(
id = CommonStrings.screen_notification_settings_system_notifications_action_required,
@ -240,7 +239,7 @@ private fun InvalidNotificationSettingsView(
}
}
@DayNightPreviews
@PreviewsDayNight
@Composable
internal fun NotificationSettingsViewPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) = ElementPreview {
NotificationSettingsView(
@ -250,7 +249,7 @@ internal fun NotificationSettingsViewPreview(@PreviewParameter(NotificationSetti
)
}
@DayNightPreviews
@PreviewsDayNight
@Composable
internal fun InvalidNotificationSettingsViewPreview() = ElementPreview {
InvalidNotificationSettingsView(

View file

@ -27,7 +27,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.RadioButton
import io.element.android.libraries.designsystem.theme.components.Text
@ -79,7 +79,7 @@ fun DefaultNotificationSettingOption(
}
}
@DayNightPreviews
@PreviewsDayNight
@Composable
internal fun DefaultNotificationSettingOptionPreview() = ElementPreview {
Column {

View file

@ -30,12 +30,12 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.ui.strings.CommonStrings
@ -56,7 +56,7 @@ fun EditDefaultNotificationSettingView(
} else {
CommonStrings.screen_notification_settings_group_chats
}
PreferenceView(
PreferencePage(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = title)
@ -133,8 +133,7 @@ fun EditDefaultNotificationSettingView(
}
}
}
@DayNightPreviews
@PreviewsDayNight
@Composable
internal fun EditDefaultNotificationSettingViewPreview(
@PreviewParameter(EditDefaultNotificationSettingsStateProvider::class) state: EditDefaultNotificationSettingState

View file

@ -27,8 +27,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.logout.api.LogoutPreferencePresenter
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient

View file

@ -17,7 +17,7 @@
package io.element.android.features.preferences.impl.root
import io.element.android.features.logout.api.LogoutPreferenceState
import io.element.android.libraries.designsystem.utils.SnackbarMessage
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.user.MatrixUser
data class PreferencesRootState(

View file

@ -17,7 +17,7 @@
package io.element.android.features.preferences.impl.root
import io.element.android.features.logout.api.aLogoutPreferenceState
import io.element.android.libraries.designsystem.utils.SnackbarMessage
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.ui.strings.CommonStrings
fun aPreferencesRootState() = PreferencesRootState(

View file

@ -20,13 +20,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.material.icons.outlined.DeveloperMode
import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.InsertChart
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.VerifiedUser
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -37,14 +31,15 @@ import androidx.compose.ui.unit.dp
import io.element.android.features.logout.api.LogoutPreferenceView
import io.element.android.features.preferences.impl.user.UserPreferences
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.LargeHeightPreview
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.SnackbarHost
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
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.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserProvider
import io.element.android.libraries.theme.ElementTheme
@ -69,7 +64,7 @@ fun PreferencesRootView(
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
// Include pref from other modules
PreferenceView(
PreferencePage(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.common_settings),
@ -92,7 +87,7 @@ fun PreferencesRootView(
if (state.accountManagementUrl != null) {
PreferenceText(
title = stringResource(id = CommonStrings.action_manage_account),
icon = Icons.Outlined.OpenInNew,
iconResourceId = CommonDrawables.ic_compound_pop_out,
onClick = { onManageAccountClicked(state.accountManagementUrl) },
)
HorizontalDivider()
@ -107,32 +102,32 @@ fun PreferencesRootView(
if (state.showNotificationSettings) {
PreferenceText(
title = stringResource(id = CommonStrings.screen_notification_settings_title),
icon = Icons.Outlined.Notifications,
iconResourceId = CommonDrawables.ic_compound_notifications,
onClick = onOpenNotificationSettings,
)
}
PreferenceText(
title = stringResource(id = CommonStrings.action_report_bug),
icon = Icons.Outlined.BugReport,
iconResourceId = CommonDrawables.ic_compound_chat_problem,
onClick = onOpenRageShake
)
PreferenceText(
title = stringResource(id = CommonStrings.common_about),
icon = Icons.Outlined.Help,
iconResourceId = CommonDrawables.ic_compound_info,
onClick = onOpenAbout,
)
HorizontalDivider()
if (state.devicesManagementUrl != null) {
PreferenceText(
title = stringResource(id = CommonStrings.action_manage_devices),
icon = Icons.Outlined.OpenInNew,
iconResourceId = CommonDrawables.ic_compound_pop_out,
onClick = { onManageAccountClicked(state.devicesManagementUrl) },
)
HorizontalDivider()
}
PreferenceText(
title = stringResource(id = CommonStrings.common_advanced_settings),
icon = Icons.Outlined.Settings,
iconResourceId = CommonDrawables.ic_compound_settings,
onClick = onOpenAdvancedSettings,
)
if (state.showDeveloperSettings) {
@ -156,20 +151,20 @@ fun PreferencesRootView(
}
@Composable
fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
PreferenceText(
title = stringResource(id = CommonStrings.common_developer_options),
icon = Icons.Outlined.DeveloperMode,
iconResourceId = CommonDrawables.ic_developer_mode,
onClick = onOpenDeveloperSettings
)
}
@LargeHeightPreview
@PreviewWithLargeHeight
@Composable
internal fun PreferencesRootViewLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
ElementPreviewLight { ContentToPreview(matrixUser) }
@LargeHeightPreview
@PreviewWithLargeHeight
@Composable
internal fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
ElementPreviewDark { ContentToPreview(matrixUser) }

View file

@ -19,7 +19,7 @@ package io.element.android.features.preferences.impl.user
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserHeader
@ -36,7 +36,7 @@ fun UserPreferences(
)
}
@DayNightPreviews
@PreviewsDayNight
@Composable
internal fun UserPreferencesPreview(@PreviewParameter(MatrixUserWithNullProvider::class) matrixUser: MatrixUser?) = ElementPreview {
UserPreferences(matrixUser)

View file

@ -18,6 +18,7 @@ package io.element.android.features.preferences.impl.user.editprofile
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
@ -39,6 +40,8 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
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 io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -49,8 +52,12 @@ class EditUserProfilePresenter @AssistedInject constructor(
private val matrixClient: MatrixClient,
private val mediaPickerProvider: PickerProvider,
private val mediaPreProcessor: MediaPreProcessor,
permissionsPresenterFactory: PermissionsPresenter.Factory,
) : Presenter<EditUserProfileState> {
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
private var pendingPermissionRequest = false
@AssistedFactory
interface Factory {
fun create(matrixUser: MatrixUser): EditUserProfilePresenter
@ -58,6 +65,7 @@ class EditUserProfilePresenter @AssistedInject constructor(
@Composable
override fun present(): EditUserProfileState {
val cameraPermissionState = cameraPermissionPresenter.present()
var userAvatarUri by rememberSaveable { mutableStateOf(matrixUser.avatarUrl?.let { Uri.parse(it) }) }
var userDisplayName by rememberSaveable { mutableStateOf(matrixUser.displayName) }
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
@ -77,6 +85,13 @@ class EditUserProfilePresenter @AssistedInject constructor(
}
}
LaunchedEffect(cameraPermissionState.permissionGranted) {
if (cameraPermissionState.permissionGranted && pendingPermissionRequest) {
pendingPermissionRequest = false
cameraPhotoPicker.launch()
}
}
val saveAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
val localCoroutineScope = rememberCoroutineScope()
fun handleEvents(event: EditUserProfileEvents) {
@ -85,7 +100,12 @@ class EditUserProfilePresenter @AssistedInject constructor(
is EditUserProfileEvents.HandleAvatarAction -> {
when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
AvatarAction.TakePhoto -> cameraPhotoPicker.launch()
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
cameraPhotoPicker.launch()
} else {
pendingPermissionRequest = true
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
}
AvatarAction.Remove -> userAvatarUri = null
}
}
@ -108,6 +128,7 @@ class EditUserProfilePresenter @AssistedInject constructor(
avatarActions = avatarActions,
saveButtonEnabled = canSave && saveAction.value !is Async.Loading,
saveAction = saveAction.value,
cameraPermissionState = cameraPermissionState,
eventSink = { handleEvents(it) },
)
}

View file

@ -20,6 +20,7 @@ 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 io.element.android.libraries.permissions.api.PermissionsState
import kotlinx.collections.immutable.ImmutableList
data class EditUserProfileState(
@ -29,5 +30,6 @@ data class EditUserProfileState(
val avatarActions: ImmutableList<AvatarAction>,
val saveButtonEnabled: Boolean,
val saveAction: Async<Unit>,
val cameraPermissionState: PermissionsState,
val eventSink: (EditUserProfileEvents) -> Unit
)

View file

@ -19,6 +19,7 @@ package io.element.android.features.preferences.impl.user.editprofile
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.permissions.api.aPermissionsState
import kotlinx.collections.immutable.persistentListOf
open class EditUserProfileStateProvider : PreviewParameterProvider<EditUserProfileState> {
@ -36,5 +37,6 @@ fun aEditUserProfileState() = EditUserProfileState(
avatarActions = persistentListOf(),
saveAction = Async.Uninitialized,
saveButtonEnabled = true,
cameraPermissionState = aPermissionsState(showDialog = false),
eventSink = {}
)

View file

@ -49,7 +49,7 @@ import io.element.android.libraries.designsystem.components.ProgressDialog
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.DayNightPreviews
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -58,6 +58,7 @@ 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.EditableAvatarView
import io.element.android.libraries.permissions.api.PermissionsView
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@ -168,6 +169,9 @@ fun EditUserProfileView(
else -> Unit
}
}
PermissionsView(
state = state.cameraPermissionState,
)
}
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
@ -177,7 +181,7 @@ private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
})
}
@DayNightPreviews
@PreviewsDayNight
@Composable
internal fun EditUserProfileViewPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) =
ElementPreview {

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_developer_mode">"Vývojářský režim"</string>
<string name="screen_advanced_settings_developer_mode_description">"Povolením získáte přístup k funkcím a funkcím pro vývojáře."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Vypněte editor formátovaného textu pro ruční zadání Markdown."</string>
<string name="screen_edit_profile_display_name">"Zobrazované jméno"</string>
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované jméno"</string>
<string name="screen_edit_profile_error">"Došlo k neznámé chybě a informace nelze změnit."</string>
<string name="screen_edit_profile_error_title">"Nelze aktualizovat profil"</string>
<string name="screen_edit_profile_title">"Upravit profil"</string>
<string name="screen_edit_profile_updating_details">"Aktualizace profilu…"</string>
</resources>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_developer_mode">"Entwickler-Modus"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Deaktiviere den Rich-Text-Editor, um Markdown manuell einzugeben."</string>
<string name="screen_edit_profile_display_name">"Anzeigename"</string>
<string name="screen_edit_profile_display_name_placeholder">"Dein Anzeigename"</string>
<string name="screen_edit_profile_error">"Ein unbekannter Fehler ist aufgetreten und die Informationen konnten nicht geändert werden."</string>

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_developer_mode">"Mode développeur"</string>
<string name="screen_advanced_settings_developer_mode_description">"Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Désactivez léditeur de texte enrichi pour saisir manuellement du Markdown."</string>
<string name="screen_edit_profile_display_name">"Pseudonyme"</string>
<string name="screen_edit_profile_display_name_placeholder">"Votre pseudonyme"</string>
<string name="screen_edit_profile_error">"Une erreur inconnue sest produite et les informations nont pas pu être modifiées."</string>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_developer_mode">"Режим разработчика"</string>
<string name="screen_advanced_settings_developer_mode_description">"Предоставьте разработчикам доступ к функциям и функциональным возможностям."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Отключить редактор форматированного текста и включить Markdown."</string>
<string name="screen_edit_profile_display_name">"Отображаемое имя"</string>
<string name="screen_edit_profile_display_name_placeholder">"Ваше отображаемое имя"</string>
<string name="screen_edit_profile_error">"Произошла неизвестная ошибка, изменить информацию не удалось."</string>
<string name="screen_edit_profile_error_title">"Невозможно обновить профиль"</string>
<string name="screen_edit_profile_title">"Редактировать профиль"</string>
<string name="screen_edit_profile_updating_details">"Обновление профиля…"</string>
</resources>

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_developer_mode">"Vývojársky režim"</string>
<string name="screen_advanced_settings_developer_mode_description">"Umožniť prístup k možnostiam a funkciám pre vývojárov."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Vypnite rozšírený textový editor na ručné písanie Markdown."</string>
<string name="screen_edit_profile_display_name">"Zobrazované meno"</string>
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované meno"</string>
<string name="screen_edit_profile_error">"Vyskytla sa neznáma chyba a informácie nebolo možné zmeniť."</string>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_developer_mode">"開發者模式"</string>
<string name="screen_edit_profile_display_name">"顯示名稱"</string>
<string name="screen_edit_profile_display_name_placeholder">"您的顯示名稱"</string>
<string name="screen_edit_profile_error_title">"無法更新個人檔案"</string>
<string name="screen_edit_profile_title">"編輯個人檔案"</string>
<string name="screen_edit_profile_updating_details">"正在更新個人檔案…"</string>
</resources>

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_developer_mode">"Developer mode"</string>
<string name="screen_advanced_settings_developer_mode_description">"Enable to have access to features and functionality for developers."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Disable the rich text editor to type Markdown manually."</string>
<string name="screen_edit_profile_display_name">"Display name"</string>
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string>
<string name="screen_edit_profile_error">"An unknown error was encountered and the information couldn\'t be changed."</string>

View file

@ -32,7 +32,7 @@ import kotlin.time.Duration.Companion.milliseconds
class NotificationSettingsPresenterTests {
@Test
fun `present - ensures initial state is correct`() = runTest {
val presenter = aNotificationPresenter()
val presenter = createNotificationSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -58,7 +58,7 @@ class NotificationSettingsPresenterTests {
@Test
fun `present - default group notification mode changed`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService()
val presenter = aNotificationPresenter(notificationSettingsService)
val presenter = createNotificationSettingsPresenter(notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -77,7 +77,7 @@ class NotificationSettingsPresenterTests {
@Test
fun `present - notification settings mismatched`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService()
val presenter = aNotificationPresenter(notificationSettingsService)
val presenter = createNotificationSettingsPresenter(notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -108,7 +108,7 @@ class NotificationSettingsPresenterTests {
initialEncryptedOneToOneDefaultMode = RoomNotificationMode.ALL_MESSAGES,
initialOneToOneDefaultMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
)
val presenter = aNotificationPresenter(notificationSettingsService)
val presenter = createNotificationSettingsPresenter(notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -125,7 +125,7 @@ class NotificationSettingsPresenterTests {
@Test
fun `present - set notifications enabled`() = runTest {
val presenter = aNotificationPresenter()
val presenter = createNotificationSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -145,7 +145,7 @@ class NotificationSettingsPresenterTests {
@Test
fun `present - set call notifications enabled`() = runTest {
val presenter = aNotificationPresenter()
val presenter = createNotificationSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -167,7 +167,7 @@ class NotificationSettingsPresenterTests {
@Test
fun `present - set atRoom notifications enabled`() = runTest {
val presenter = aNotificationPresenter()
val presenter = createNotificationSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -187,7 +187,7 @@ class NotificationSettingsPresenterTests {
}
}
private fun aNotificationPresenter(
private fun createNotificationSettingsPresenter(
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService()
) : NotificationSettingsPresenter {
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)

View file

@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.impl.DefaultLogoutPreferencePresenter
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL

View file

@ -32,6 +32,9 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.mockk.every
@ -78,12 +81,14 @@ class EditUserProfilePresenterTest {
private fun createEditUserProfilePresenter(
matrixClient: MatrixClient = FakeMatrixClient(),
matrixUser: MatrixUser = aMatrixUser(),
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
): EditUserProfilePresenter {
return EditUserProfilePresenter(
matrixClient = matrixClient,
matrixUser = matrixUser,
mediaPickerProvider = fakePickerProvider,
mediaPreProcessor = fakeMediaPreProcessor,
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
)
}
@ -157,16 +162,30 @@ class EditUserProfilePresenterTest {
fun `present - obtains avatar uris from camera`() = runTest {
val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL)
fakePickerProvider.givenResult(anotherAvatarUri)
val presenter = createEditUserProfilePresenter(matrixUser = user)
val fakePermissionsPresenter = FakePermissionsPresenter()
val presenter = createEditUserProfilePresenter(
matrixUser = user,
permissionsPresenter = fakePermissionsPresenter,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri)
assertThat(initialState.cameraPermissionState.permissionGranted).isFalse()
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto))
awaitItem().apply {
assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri)
}
val stateWithAskingPermission = awaitItem()
assertThat(stateWithAskingPermission.cameraPermissionState.showDialog).isTrue()
fakePermissionsPresenter.setPermissionGranted()
val stateWithPermission = awaitItem()
assertThat(stateWithPermission.cameraPermissionState.permissionGranted).isTrue()
val stateWithNewAvatar = awaitItem()
assertThat(stateWithNewAvatar.userAvatarUrl).isEqualTo(anotherAvatarUri)
// Do it again, no permission is requested
fakePickerProvider.givenResult(userAvatarUri)
stateWithNewAvatar.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto))
val stateWithNewAvatar2 = awaitItem()
assertThat(stateWithNewAvatar2.userAvatarUrl).isEqualTo(userAvatarUri)
}
}