Move logout and report bug to Setting screen

This commit is contained in:
Benoit Marty 2022-12-21 21:48:16 +01:00 committed by Benoit Marty
parent 0568ea7c28
commit 37daf12cd0
36 changed files with 429 additions and 175 deletions

1
.gitignore vendored
View file

@ -82,3 +82,4 @@ lint/generated/
lint/outputs/ lint/outputs/
lint/tmp/ lint/tmp/
# lint/reports/ # lint/reports/
/.idea/deploymentTargetDropDown.xml

View file

@ -138,6 +138,7 @@ dependencies {
implementation(project(":libraries:core")) implementation(project(":libraries:core"))
implementation(project(":features:onboarding")) implementation(project(":features:onboarding"))
implementation(project(":features:login")) implementation(project(":features:login"))
implementation(project(":features:logout"))
implementation(project(":features:roomlist")) implementation(project(":features:roomlist"))
implementation(project(":features:messages")) implementation(project(":features:messages"))
implementation(project(":features:rageshake")) implementation(project(":features:rageshake"))

View file

@ -71,7 +71,6 @@ fun ChangeServerScreenNavigation(navigator: DestinationsNavigator) {
@Destination @Destination
@Composable @Composable
fun RoomListScreenNavigation(navigator: DestinationsNavigator) { fun RoomListScreenNavigation(navigator: DestinationsNavigator) {
val sessionComponentsOwner = LocalContext.current.bindings<AppBindings>().sessionComponentsOwner()
RoomListScreen( RoomListScreen(
onRoomClicked = { roomId: RoomId -> onRoomClicked = { roomId: RoomId ->
navigator.navigate(MessagesScreenNavigationDestination(roomId = roomId.value)) navigator.navigate(MessagesScreenNavigationDestination(roomId = roomId.value))
@ -79,18 +78,6 @@ fun RoomListScreenNavigation(navigator: DestinationsNavigator) {
onOpenSettings = { onOpenSettings = {
navigator.navigate(PreferencesScreenNavigationDestination()) navigator.navigate(PreferencesScreenNavigationDestination())
}, },
onSuccessLogout = {
sessionComponentsOwner.releaseActiveSession()
navigator.navigate(OnBoardingScreenNavigationDestination) {
popUpTo(RoomListScreenNavigationDestination) {
inclusive = true
}
}
},
// Tmp entry point
onOpenRageShake = {
navigator.navigate(BugReportScreenNavigationDestination)
}
) )
} }
@ -111,8 +98,19 @@ fun BugReportScreenNavigation(navigator: DestinationsNavigator) {
@Destination @Destination
@Composable @Composable
fun PreferencesScreenNavigation(navigator: DestinationsNavigator) { fun PreferencesScreenNavigation(navigator: DestinationsNavigator) {
val sessionComponentsOwner = LocalContext.current.bindings<AppBindings>().sessionComponentsOwner()
PreferencesScreen( PreferencesScreen(
onBackPressed = navigator::navigateUp onBackPressed = navigator::navigateUp,
onOpenRageShake = {
navigator.navigate(BugReportScreenNavigationDestination)
},
onSuccessLogout = {
sessionComponentsOwner.releaseActiveSession()
navigator.navigate(OnBoardingScreenNavigationDestination) {
popUpTo(RoomListScreenNavigationDestination) {
inclusive = true
}
}
},
) )
} }

1
features/logout/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,27 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.ksp)
alias(libs.plugins.anvil)
}
android {
namespace = "io.element.android.x.features.logout"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
implementation(project(":anvilannotations"))
anvil(project(":anvilcodegen"))
implementation(project(":libraries:di"))
implementation(project(":libraries:core"))
implementation(project(":libraries:matrix"))
implementation(project(":libraries:designsystem"))
implementation(project(":libraries:elementresources"))
implementation(libs.mavericks.compose)
ksp(libs.showkase.processor)
testImplementation(libs.test.junit)
androidTestImplementation(libs.test.junitext)
}

View file

21
features/logout/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,22 @@
package io.element.android.x.features.logout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.x.features.login.test", appContext.packageName)
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>

View file

@ -0,0 +1,84 @@
package io.element.android.x.features.logout
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Logout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.ProgressDialog
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.x.designsystem.components.preferences.PreferenceCategory
import io.element.android.x.designsystem.components.preferences.PreferenceText
import io.element.android.x.element.resources.R as ElementR
@Composable
fun LogoutPreference(
viewModel: LogoutViewModel = mavericksViewModel(),
onSuccessLogout: () -> Unit = { },
) {
val state: LogoutViewState by viewModel.collectAsState()
if (state.logoutAction is Success) {
onSuccessLogout()
return
}
val openDialog = remember { mutableStateOf(false) }
LogoutPreferenceContent(
onClick = {
openDialog.value = true
}
)
// Log out confirmation dialog
if (openDialog.value) {
ConfirmationDialog(
title = stringResource(id = ElementR.string.action_sign_out),
content = stringResource(id = ElementR.string.action_sign_out_confirmation_simple),
submitText = stringResource(id = ElementR.string.action_sign_out),
onCancelClicked = {
openDialog.value = false
},
onSubmitClicked = {
openDialog.value = false
viewModel.logout()
},
onDismiss = {
openDialog.value = false
}
)
}
if (state.logoutAction is Loading) {
ProgressDialog(text = "Login out...")
}
}
@Composable
fun LogoutPreferenceContent(
onClick: () -> Unit = {},
) {
PreferenceCategory(title = stringResource(id = ElementR.string.settings_general_title)) {
PreferenceText(
title = stringResource(id = ElementR.string.action_sign_out),
icon = Icons.Default.Logout,
onClick = onClick
)
}
}
@Composable
@Preview
fun LogoutContentPreview() {
ElementXTheme(darkTheme = false) {
LogoutPreference()
}
}

View file

@ -0,0 +1,30 @@
package io.element.android.x.features.logout
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.core.di.daggerMavericksViewModelFactory
import io.element.android.x.di.SessionScope
import io.element.android.x.matrix.MatrixClient
import kotlinx.coroutines.launch
@ContributesViewModel(SessionScope::class)
class LogoutViewModel @AssistedInject constructor(
private val client: MatrixClient,
@Assisted initialState: LogoutViewState
) : MavericksViewModel<LogoutViewState>(initialState) {
companion object : MavericksViewModelFactory<LogoutViewModel, LogoutViewState> by daggerMavericksViewModelFactory()
fun logout() {
viewModelScope.launch {
suspend {
client.logout()
}.execute {
copy(logoutAction = it)
}
}
}
}

View file

@ -0,0 +1,9 @@
package io.element.android.x.features.logout
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
data class LogoutViewState(
val logoutAction: Async<Unit> = Uninitialized,
) : MavericksState

View file

@ -0,0 +1,16 @@
package io.element.android.x.features.logout
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View file

@ -18,6 +18,7 @@ dependencies {
implementation(project(":libraries:di")) implementation(project(":libraries:di"))
implementation(project(":libraries:core")) implementation(project(":libraries:core"))
implementation(project(":features:rageshake")) implementation(project(":features:rageshake"))
implementation(project(":features:logout"))
implementation(project(":libraries:designsystem")) implementation(project(":libraries:designsystem"))
implementation(project(":libraries:elementresources")) implementation(project(":libraries:elementresources"))
implementation(libs.mavericks.compose) implementation(libs.mavericks.compose)

View file

@ -1,37 +1,43 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.x.features.preferences package io.element.android.x.features.preferences
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import io.element.android.x.designsystem.components.preferences.PreferenceScreen import io.element.android.x.designsystem.components.preferences.PreferenceScreen
import io.element.android.x.features.rageshake.preferences.RageshakePreferenceCategory
import io.element.android.x.element.resources.R as ElementR import io.element.android.x.element.resources.R as ElementR
import io.element.android.x.features.logout.LogoutPreference
import io.element.android.x.features.rageshake.preferences.RageshakePreferences
@Composable @Composable
fun PreferencesScreen( fun PreferencesScreen(
onBackPressed: () -> Unit = {}, onBackPressed: () -> Unit = {},
onOpenRageShake: () -> Unit = {},
onSuccessLogout: () -> Unit = {},
) { ) {
// TODO Hierarchy! // TODO Hierarchy!
// TODO Move logout here
// Include pref from other modules // Include pref from other modules
PreferencesContent(onBackPressed = onBackPressed) PreferencesContent(
onBackPressed = onBackPressed,
onOpenRageShake = onOpenRageShake,
onSuccessLogout = onSuccessLogout,
)
} }
@Composable @Composable
fun PreferencesContent( fun PreferencesContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {}, onBackPressed: () -> Unit = {},
onOpenRageShake: () -> Unit = {},
onSuccessLogout: () -> Unit = {},
) { ) {
PreferenceScreen( PreferenceScreen(
modifier = modifier, modifier = modifier,
onBackPressed = onBackPressed, onBackPressed = onBackPressed,
title = stringResource(id = ElementR.string.settings) title = stringResource(id = ElementR.string.settings)
) { ) {
RageshakePreferenceCategory() LogoutPreference(onSuccessLogout = onSuccessLogout)
RageshakePreferences(onOpenRageShake = onOpenRageShake)
} }
} }

View file

@ -4,7 +4,6 @@ package io.element.android.x.features.rageshake.bugreport
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
@ -14,7 +13,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme

View file

@ -9,10 +9,10 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import io.element.android.x.core.bool.orFalse import io.element.android.x.core.bool.orFalse
import io.element.android.x.di.ApplicationContext import io.element.android.x.di.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import javax.inject.Inject
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_crash") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_crash")

View file

@ -3,9 +3,9 @@ package io.element.android.x.features.rageshake.crash
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import io.element.android.x.core.data.tryOrNull import io.element.android.x.core.data.tryOrNull
import timber.log.Timber
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import timber.log.Timber
class VectorUncaughtExceptionHandler( class VectorUncaughtExceptionHandler(
context: Context context: Context

View file

@ -3,17 +3,17 @@ package io.element.android.x.features.rageshake.logs
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import io.element.android.x.core.data.tryOrNull import io.element.android.x.core.data.tryOrNull
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File import java.io.File
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.util.logging.FileHandler import java.util.logging.FileHandler
import java.util.logging.Level import java.util.logging.Level
import java.util.logging.Logger import java.util.logging.Logger
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
/** /**
* Will be planted in Timber. * Will be planted in Timber.

View file

@ -1,7 +1,11 @@
package io.element.android.x.features.rageshake.preferences package io.element.android.x.features.rageshake.preferences
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.collectAsState
@ -10,44 +14,58 @@ import io.element.android.x.designsystem.components.preferences.PreferenceCatego
import io.element.android.x.designsystem.components.preferences.PreferenceSlide import io.element.android.x.designsystem.components.preferences.PreferenceSlide
import io.element.android.x.designsystem.components.preferences.PreferenceSwitch import io.element.android.x.designsystem.components.preferences.PreferenceSwitch
import io.element.android.x.designsystem.components.preferences.PreferenceText import io.element.android.x.designsystem.components.preferences.PreferenceText
import io.element.android.x.element.resources.R as ElementR
import io.element.android.x.features.rageshake.detection.RageshakeDetectionViewModel import io.element.android.x.features.rageshake.detection.RageshakeDetectionViewModel
import io.element.android.x.features.rageshake.detection.RageshakeDetectionViewState import io.element.android.x.features.rageshake.detection.RageshakeDetectionViewState
import io.element.android.x.element.resources.R as ElementR
@Composable @Composable
fun RageshakePreferenceCategory() { fun RageshakePreferences(
RageshakePreferenceContent() onOpenRageShake: () -> Unit = {},
) {
RageshakePreferencesContent(
onOpenRageShake = onOpenRageShake,
)
} }
@Composable @Composable
fun RageshakePreferenceContent( fun RageshakePreferencesContent(
viewModel: RageshakeDetectionViewModel = mavericksViewModel() modifier: Modifier = Modifier,
viewModel: RageshakeDetectionViewModel = mavericksViewModel(),
onOpenRageShake: () -> Unit = {},
) { ) {
val state: RageshakeDetectionViewState by viewModel.collectAsState() val state: RageshakeDetectionViewState by viewModel.collectAsState()
PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) { Column(modifier = modifier) {
if (state.isSupported) { PreferenceCategory(title = stringResource(id = ElementR.string.send_bug_report)) {
PreferenceSwitch( PreferenceText(
title = stringResource(id = ElementR.string.send_bug_report_rage_shake), title = stringResource(id = ElementR.string.send_bug_report),
isChecked = state.isEnabled, icon = Icons.Default.BugReport,
onCheckedChange = viewModel::onEnableClicked onClick = onOpenRageShake
) )
if (state.isEnabled) { }
PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) {
if (state.isSupported) {
PreferenceSwitch(
title = stringResource(id = ElementR.string.send_bug_report_rage_shake),
isChecked = state.isEnabled,
onCheckedChange = viewModel::onEnableClicked
)
PreferenceSlide( PreferenceSlide(
title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold), title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold),
// summary = stringResource(id = ElementR.string.settings_rageshake_detection_threshold_summary), // summary = stringResource(id = ElementR.string.settings_rageshake_detection_threshold_summary),
value = state.sensitivity, value = state.sensitivity,
enabled = state.isEnabled,
steps = 3 /* 5 possible values - steps are in ]0, 1[ */, steps = 3 /* 5 possible values - steps are in ]0, 1[ */,
onValueChange = viewModel::onSensitivityChange onValueChange = viewModel::onSensitivityChange
) )
} else {
PreferenceText(title = "Rageshaking is not supported by your device")
} }
} else {
PreferenceText(title = "Rageshaking is not supported by your device")
} }
} }
} }
@Composable @Composable
@Preview @Preview
fun RageshakePreferenceCategoryPreview() { fun RageshakePreferencePreview() {
RageshakePreferenceCategory() RageshakePreferences()
} }

View file

@ -41,7 +41,8 @@ class RageShake @Inject constructor(
} }
/** /**
* sensitivity will be {0, O.25, 0.5, 0.75, 1} and converted to [ShakeDetector.SENSITIVITY_LIGHT (=11), ShakeDetector.SENSITIVITY_HARD (=15)] * sensitivity will be {0, O.25, 0.5, 0.75, 1} and converted to
* [ShakeDetector.SENSITIVITY_LIGHT (=11), ShakeDetector.SENSITIVITY_HARD (=15)].
*/ */
fun setSensitivity(sensitivity: Float) { fun setSensitivity(sensitivity: Float) {
shakeDetector?.setSensitivity( shakeDetector?.setSensitivity(

View file

@ -9,9 +9,9 @@ import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import io.element.android.x.core.bool.orTrue import io.element.android.x.core.bool.orTrue
import io.element.android.x.di.ApplicationContext import io.element.android.x.di.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_rageshake") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_rageshake")

View file

@ -10,6 +10,12 @@ import io.element.android.x.features.rageshake.R
import io.element.android.x.features.rageshake.crash.CrashDataStore import io.element.android.x.features.rageshake.crash.CrashDataStore
import io.element.android.x.features.rageshake.logs.VectorFileLogger import io.element.android.x.features.rageshake.logs.VectorFileLogger
import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder
import java.io.File
import java.io.IOException
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.util.Locale
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -24,12 +30,6 @@ import okhttp3.Response
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.io.IOException
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.util.Locale
import javax.inject.Inject
/** /**
* BugReporter creates and sends the bug reports. * BugReporter creates and sends the bug reports.
@ -121,6 +121,7 @@ class BugReporter @Inject constructor(
/** /**
* Send a bug report. * Send a bug report.
* *
* @param coroutineScope The coroutine scope
* @param reportType The report type (bug, suggestion, feedback) * @param reportType The report type (bug, suggestion, feedback)
* @param withDevicesLogs true to include the device log * @param withDevicesLogs true to include the device log
* @param withCrashLogs true to include the crash logs * @param withCrashLogs true to include the crash logs
@ -241,14 +242,14 @@ class BugReporter @Inject constructor(
.addFormDataPart("device", Build.MODEL.trim()) .addFormDataPart("device", Build.MODEL.trim())
// .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff()) // .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff())
.addFormDataPart("multi_window", inMultiWindowMode.toOnOff()) .addFormDataPart("multi_window", inMultiWindowMode.toOnOff())
//.addFormDataPart( // .addFormDataPart(
// "os", Build.VERSION.RELEASE + " (API " + sdkIntProvider.get() + ") " + // "os", Build.VERSION.RELEASE + " (API " + sdkIntProvider.get() + ") " +
// Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME // Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME
//) // )
.addFormDataPart("locale", Locale.getDefault().toString()) .addFormDataPart("locale", Locale.getDefault().toString())
//.addFormDataPart("app_language", vectorLocale.applicationLocale.toString()) // .addFormDataPart("app_language", vectorLocale.applicationLocale.toString())
//.addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) // .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString())
//.addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) // .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context))
.addFormDataPart("server_version", serverVersion) .addFormDataPart("server_version", serverVersion)
.apply { .apply {
customFields?.forEach { (name, value) -> customFields?.forEach { (name, value) ->
@ -442,19 +443,19 @@ class BugReporter @Inject constructor(
} }
*/ */
//private fun logOtherInfo() { // private fun logOtherInfo() {
// Timber.i("SyncThread state: " + activeSessionHolder.getSafeActiveSession()?.syncService()?.getSyncState()) // Timber.i("SyncThread state: " + activeSessionHolder.getSafeActiveSession()?.syncService()?.getSyncState())
//} // }
//private fun logDbInfo() { // private fun logDbInfo() {
// val dbInfo = matrix.debugService().getDbUsageInfo() // val dbInfo = matrix.debugService().getDbUsageInfo()
// Timber.i(dbInfo) // Timber.i(dbInfo)
//} // }
//private fun logProcessInfo() { // private fun logProcessInfo() {
// val pInfo = processInfo.getInfo() // val pInfo = processInfo.getInfo()
// Timber.i(pInfo) // Timber.i(pInfo)
//} // }
private fun rageShakeAppNameForReport(reportType: ReportType): String { private fun rageShakeAppNameForReport(reportType: ReportType): String {
// As per https://github.com/matrix-org/rageshake // As per https://github.com/matrix-org/rageshake

View file

@ -21,13 +21,10 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.Velocity
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.core.compose.LogCompositions import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.ProgressDialog
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.roomlist.components.RoomListTopBar import io.element.android.x.features.roomlist.components.RoomListTopBar
import io.element.android.x.features.roomlist.components.RoomSummaryRow import io.element.android.x.features.roomlist.components.RoomSummaryRow
@ -42,17 +39,10 @@ import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun RoomListScreen( fun RoomListScreen(
viewModel: RoomListViewModel = mavericksViewModel(), viewModel: RoomListViewModel = mavericksViewModel(),
onSuccessLogout: () -> Unit = { },
onRoomClicked: (RoomId) -> Unit = { }, onRoomClicked: (RoomId) -> Unit = { },
onOpenRageShake: () -> Unit = { },
onOpenSettings: () -> Unit = { }, onOpenSettings: () -> Unit = { },
) { ) {
val logoutAction by viewModel.collectAsState(RoomListViewState::logoutAction)
val filter by viewModel.collectAsState(RoomListViewState::filter) val filter by viewModel.collectAsState(RoomListViewState::filter)
if (logoutAction is Success) {
onSuccessLogout()
return
}
LogCompositions(tag = "RoomListScreen", msg = "Root") LogCompositions(tag = "RoomListScreen", msg = "Root")
val roomSummaries by viewModel.collectAsState(RoomListViewState::rooms) val roomSummaries by viewModel.collectAsState(RoomListViewState::rooms)
val matrixUser by viewModel.collectAsState(RoomListViewState::user) val matrixUser by viewModel.collectAsState(RoomListViewState::user)
@ -60,10 +50,7 @@ fun RoomListScreen(
roomSummaries = roomSummaries().orEmpty().toImmutableList(), roomSummaries = roomSummaries().orEmpty().toImmutableList(),
matrixUser = matrixUser(), matrixUser = matrixUser(),
onRoomClicked = onRoomClicked, onRoomClicked = onRoomClicked,
onLogoutClicked = viewModel::logout,
onOpenSettings = onOpenSettings, onOpenSettings = onOpenSettings,
onOpenRageShake = onOpenRageShake,
isLoginOut = logoutAction is Loading,
filter = filter, filter = filter,
onFilterChanged = viewModel::filterRoom, onFilterChanged = viewModel::filterRoom,
onScrollOver = viewModel::updateVisibleRange onScrollOver = viewModel::updateVisibleRange
@ -75,13 +62,10 @@ fun RoomListContent(
roomSummaries: ImmutableList<RoomListRoomSummary>, roomSummaries: ImmutableList<RoomListRoomSummary>,
matrixUser: MatrixUser?, matrixUser: MatrixUser?,
filter: String, filter: String,
isLoginOut: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onRoomClicked: (RoomId) -> Unit = {}, onRoomClicked: (RoomId) -> Unit = {},
onFilterChanged: (String) -> Unit = {}, onFilterChanged: (String) -> Unit = {},
onLogoutClicked: () -> Unit = {},
onOpenSettings: () -> Unit = {}, onOpenSettings: () -> Unit = {},
onOpenRageShake: () -> Unit = { },
onScrollOver: (IntRange) -> Unit = {}, onScrollOver: (IntRange) -> Unit = {},
) { ) {
fun onRoomClicked(room: RoomListRoomSummary) { fun onRoomClicked(room: RoomListRoomSummary) {
@ -118,9 +102,7 @@ fun RoomListContent(
matrixUser = matrixUser, matrixUser = matrixUser,
filter = filter, filter = filter,
onFilterChanged = onFilterChanged, onFilterChanged = onFilterChanged,
onLogoutClicked = onLogoutClicked,
onOpenSettings = onOpenSettings, onOpenSettings = onOpenSettings,
onOpenRageShake = onOpenRageShake,
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
}, },
@ -142,9 +124,6 @@ fun RoomListContent(
} }
} }
) )
if (isLoginOut) {
ProgressDialog(text = "Login out...")
}
} }
private fun RoomListRoomSummary.contentType() = isPlaceholder private fun RoomListRoomSummary.contentType() = isPlaceholder
@ -161,10 +140,8 @@ fun PreviewableRoomListContent() {
roomSummaries = stubbedRoomSummaries(), roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")), matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
onRoomClicked = {}, onRoomClicked = {},
onLogoutClicked = {},
filter = "filter", filter = "filter",
onFilterChanged = {}, onFilterChanged = {},
isLoginOut = false,
onScrollOver = {} onScrollOver = {}
) )
} }
@ -178,10 +155,8 @@ fun PreviewableDarkRoomListContent() {
roomSummaries = stubbedRoomSummaries(), roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")), matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
onRoomClicked = {}, onRoomClicked = {},
onLogoutClicked = {},
filter = "filter", filter = "filter",
onFilterChanged = {}, onFilterChanged = {},
isLoginOut = true,
onScrollOver = {} onScrollOver = {}
) )
} }

View file

@ -20,7 +20,6 @@ import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.RoomSummary import io.element.android.x.matrix.room.RoomSummary
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@ -33,8 +32,7 @@ private const val extendedRangeSize = 40
class RoomListViewModel @AssistedInject constructor( class RoomListViewModel @AssistedInject constructor(
private val client: MatrixClient, private val client: MatrixClient,
@Assisted initialState: RoomListViewState @Assisted initialState: RoomListViewState
) : ) : MavericksViewModel<RoomListViewState>(initialState) {
MavericksViewModel<RoomListViewState>(initialState) {
companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by daggerMavericksViewModelFactory() companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by daggerMavericksViewModelFactory()
@ -44,17 +42,6 @@ class RoomListViewModel @AssistedInject constructor(
handleInit() handleInit()
} }
fun logout() {
viewModelScope.launch {
suspend {
delay(2000)
client.logout()
}.execute {
copy(logoutAction = it)
}
}
}
fun filterRoom(filter: String) { fun filterRoom(filter: String) {
setState { setState {
copy( copy(

View file

@ -7,9 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.ContentAlpha import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Logout
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -34,25 +32,20 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.element.android.x.core.compose.LogCompositions import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.core.compose.textFieldState import io.element.android.x.core.compose.textFieldState
import io.element.android.x.designsystem.components.avatar.Avatar import io.element.android.x.designsystem.components.avatar.Avatar
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.x.features.roomlist.model.MatrixUser import io.element.android.x.features.roomlist.model.MatrixUser
import io.element.android.x.element.resources.R as ElementR
@Composable @Composable
fun RoomListTopBar( fun RoomListTopBar(
matrixUser: MatrixUser?, matrixUser: MatrixUser?,
filter: String, filter: String,
onFilterChanged: (String) -> Unit, onFilterChanged: (String) -> Unit,
onLogoutClicked: () -> Unit,
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
onOpenRageShake: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior scrollBehavior: TopAppBarScrollBehavior
) { ) {
LogCompositions(tag = "RoomListScreen", msg = "TopBar") LogCompositions(tag = "RoomListScreen", msg = "TopBar")
@ -77,9 +70,7 @@ fun RoomListTopBar(
} else { } else {
DefaultRoomListTopBar( DefaultRoomListTopBar(
matrixUser = matrixUser, matrixUser = matrixUser,
onLogoutClicked = onLogoutClicked,
onOpenSettings = onOpenSettings, onOpenSettings = onOpenSettings,
onOpenRageShake = onOpenRageShake,
onSearchClicked = { onSearchClicked = {
searchWidgetStateIsOpened = true searchWidgetStateIsOpened = true
}, },
@ -168,13 +159,10 @@ fun SearchRoomListTopBar(
@Composable @Composable
private fun DefaultRoomListTopBar( private fun DefaultRoomListTopBar(
matrixUser: MatrixUser?, matrixUser: MatrixUser?,
onLogoutClicked: () -> Unit,
onOpenRageShake: () -> Unit,
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
onSearchClicked: () -> Unit, onSearchClicked: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior scrollBehavior: TopAppBarScrollBehavior
) { ) {
val openDialog = remember { mutableStateOf(false) }
MediumTopAppBar( MediumTopAppBar(
modifier = Modifier modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection), .nestedScroll(scrollBehavior.nestedScrollConnection),
@ -197,16 +185,6 @@ private fun DefaultRoomListTopBar(
) { ) {
Icon(Icons.Default.Search, contentDescription = "search") Icon(Icons.Default.Search, contentDescription = "search")
} }
IconButton(
onClick = onOpenRageShake
) {
Icon(Icons.Default.BugReport, contentDescription = stringResource(id = ElementR.string.send_bug_report))
}
IconButton(
onClick = { openDialog.value = true }
) {
Icon(Icons.Default.Logout, contentDescription = "logout")
}
IconButton( IconButton(
onClick = onOpenSettings onClick = onOpenSettings
) { ) {
@ -215,19 +193,4 @@ private fun DefaultRoomListTopBar(
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
// Log out confirmation dialog
if (openDialog.value) {
ConfirmationDialog(
title = "Log out",
content = "Do you confirm you want to log out?",
submitText = "Log out",
onSubmitClicked = {
openDialog.value = false
onLogoutClicked()
},
onDismiss = {
openDialog.value = false
}
)
}
} }

View file

@ -11,6 +11,5 @@ data class RoomListViewState(
val rooms: Async<List<RoomListRoomSummary>> = Uninitialized, val rooms: Async<List<RoomListRoomSummary>> = Uninitialized,
val filter: String = "", val filter: String = "",
val canLoadMore: Boolean = false, val canLoadMore: Boolean = false,
val logoutAction: Async<Unit> = Uninitialized,
val roomsById: Map<RoomId, RoomListRoomSummary> = emptyMap() val roomsById: Map<RoomId, RoomListRoomSummary> = emptyMap()
) : MavericksState ) : MavericksState

View file

@ -1,8 +1,8 @@
package io.element.android.x.core.file package io.element.android.x.core.file
import timber.log.Timber
import java.io.File import java.io.File
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
import timber.log.Timber
/** /**
* GZip a file. * GZip a file.

View file

@ -0,0 +1,14 @@
package io.element.android.x.designsystem
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
@Composable
fun Boolean.toEnabledColor(): Color {
return if (this) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.secondary
}
}

View file

@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.x.element.resources.R as ElementR import io.element.android.x.element.resources.R as ElementR
@Composable @Composable

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -20,9 +21,14 @@ fun PreferenceCategory(
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp)
) { ) {
Divider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.secondary,
thickness = 1.dp
)
Text( Text(
modifier = Modifier.padding(top = 8.dp, start = 56.dp),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
text = title text = title
) )

View file

@ -3,6 +3,7 @@ package io.element.android.x.designsystem.components.preferences
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -11,7 +12,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import io.element.android.x.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.x.designsystem.toEnabledColor
@Composable @Composable
fun PreferenceSlide( fun PreferenceSlide(
@ -19,6 +23,8 @@ fun PreferenceSlide(
@FloatRange(0.0, 1.0) @FloatRange(0.0, 1.0)
value: Float, value: Float,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: ImageVector? = null,
enabled: Boolean = true,
summary: String? = null, summary: String? = null,
steps: Int = 0, steps: Int = 0,
onValueChange: (Float) -> Unit = {}, onValueChange: (Float) -> Unit = {},
@ -29,27 +35,32 @@ fun PreferenceSlide(
.defaultMinSize(minHeight = preferenceMinHeight), .defaultMinSize(minHeight = preferenceMinHeight),
contentAlignment = Alignment.CenterStart contentAlignment = Alignment.CenterStart
) { ) {
Column( Row(modifier = Modifier.fillMaxWidth()) {
modifier = modifier PreferenceIcon(icon = icon)
.fillMaxWidth(), Column(
) { modifier = Modifier.weight(1f),
Text( ) {
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyLarge,
text = title
)
summary?.let {
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyLarge,
text = summary color = enabled.toEnabledColor(),
text = title
)
summary?.let {
Text(
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium,
color = enabled.toEnabledColor(),
text = summary
)
}
Slider(
value = value,
steps = steps,
onValueChange = onValueChange,
enabled = enabled,
) )
} }
Slider(
value = value,
steps = steps,
onValueChange = onValueChange
)
} }
} }
} }

View file

@ -1,42 +1,58 @@
package io.element.android.x.designsystem.components.preferences package io.element.android.x.designsystem.components.preferences
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Announcement
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import io.element.android.x.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.x.designsystem.toEnabledColor
@Composable @Composable
fun PreferenceSwitch( fun PreferenceSwitch(
title: String, title: String,
isChecked: Boolean, isChecked: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
onCheckedChange: (Boolean) -> Unit = {}, onCheckedChange: (Boolean) -> Unit = {},
) { ) {
Box( Box(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight), .defaultMinSize(minHeight = preferenceMinHeight)
.clickable { onCheckedChange(!isChecked) },
contentAlignment = Alignment.CenterStart contentAlignment = Alignment.CenterStart
) { ) {
Row( Row(
modifier = modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
PreferenceIcon(
icon = icon,
enabled = enabled
)
Text( Text(
modifier = modifier modifier = Modifier.weight(1f),
.weight(1f),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = enabled.toEnabledColor(),
text = title text = title
) )
Checkbox(checked = isChecked, onCheckedChange = onCheckedChange) Checkbox(
checked = isChecked,
enabled = enabled,
onCheckedChange = onCheckedChange
)
} }
} }
} }
@ -46,6 +62,7 @@ fun PreferenceSwitch(
fun PreferenceSwitchPreview() { fun PreferenceSwitchPreview() {
PreferenceSwitch( PreferenceSwitch(
title = "Switch", title = "Switch",
icon = Icons.Default.Announcement,
isChecked = true isChecked = true
) )
} }

View file

@ -2,20 +2,26 @@ package io.element.android.x.designsystem.components.preferences
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.Row
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import io.element.android.x.designsystem.components.preferences.components.PreferenceIcon
@Composable @Composable
fun PreferenceText( fun PreferenceText(
title: String, title: String,
// TODO subtitle // TODO subtitle
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: ImageVector? = null,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
Box( Box(
@ -25,12 +31,17 @@ fun PreferenceText(
.clickable { onClick() }, .clickable { onClick() },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth()
.fillMaxWidth(), ) {
style = MaterialTheme.typography.bodyLarge, PreferenceIcon(icon = icon)
text = title Text(
) modifier = Modifier
.weight(1f),
style = MaterialTheme.typography.bodyLarge,
text = title
)
}
} }
} }
@ -39,5 +50,6 @@ fun PreferenceText(
fun PreferenceTextPreview() { fun PreferenceTextPreview() {
PreferenceText( PreferenceText(
title = "Title", title = "Title",
icon = Icons.Default.BugReport,
) )
} }

View file

@ -0,0 +1,31 @@
package io.element.android.x.designsystem.components.preferences.components
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.element.android.x.designsystem.toEnabledColor
@Composable
fun PreferenceIcon(
icon: ImageVector?,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = "",
tint = enabled.toEnabledColor(),
modifier = modifier
.padding(start = 8.dp)
.width(48.dp),
)
} else {
Spacer(modifier = modifier.width(56.dp))
}
}

View file

@ -25,6 +25,7 @@ include(":libraries:textcomposer")
include(":libraries:elementresources") include(":libraries:elementresources")
include(":features:onboarding") include(":features:onboarding")
include(":features:login") include(":features:login")
include(":features:logout")
include(":features:roomlist") include(":features:roomlist")
include(":features:messages") include(":features:messages")
include(":features:rageshake") include(":features:rageshake")