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/tmp/
# lint/reports/
/.idea/deploymentTargetDropDown.xml

View File

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

View File

@@ -71,7 +71,6 @@ fun ChangeServerScreenNavigation(navigator: DestinationsNavigator) {
@Destination
@Composable
fun RoomListScreenNavigation(navigator: DestinationsNavigator) {
val sessionComponentsOwner = LocalContext.current.bindings<AppBindings>().sessionComponentsOwner()
RoomListScreen(
onRoomClicked = { roomId: RoomId ->
navigator.navigate(MessagesScreenNavigationDestination(roomId = roomId.value))
@@ -79,18 +78,6 @@ fun RoomListScreenNavigation(navigator: DestinationsNavigator) {
onOpenSettings = {
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
@Composable
fun PreferencesScreenNavigation(navigator: DestinationsNavigator) {
val sessionComponentsOwner = LocalContext.current.bindings<AppBindings>().sessionComponentsOwner()
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:core"))
implementation(project(":features:rageshake"))
implementation(project(":features:logout"))
implementation(project(":libraries:designsystem"))
implementation(project(":libraries:elementresources"))
implementation(libs.mavericks.compose)

View File

@@ -1,37 +1,43 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.x.features.preferences
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
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.features.logout.LogoutPreference
import io.element.android.x.features.rageshake.preferences.RageshakePreferences
@Composable
fun PreferencesScreen(
onBackPressed: () -> Unit = {},
onOpenRageShake: () -> Unit = {},
onSuccessLogout: () -> Unit = {},
) {
// TODO Hierarchy!
// TODO Move logout here
// Include pref from other modules
PreferencesContent(onBackPressed = onBackPressed)
PreferencesContent(
onBackPressed = onBackPressed,
onOpenRageShake = onOpenRageShake,
onSuccessLogout = onSuccessLogout,
)
}
@Composable
fun PreferencesContent(
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
onOpenRageShake: () -> Unit = {},
onSuccessLogout: () -> Unit = {},
) {
PreferenceScreen(
modifier = modifier,
onBackPressed = onBackPressed,
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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme

View File

@@ -9,10 +9,10 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import io.element.android.x.core.bool.orFalse
import io.element.android.x.di.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
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.os.Build
import io.element.android.x.core.data.tryOrNull
import timber.log.Timber
import java.io.PrintWriter
import java.io.StringWriter
import timber.log.Timber
class VectorUncaughtExceptionHandler(
context: Context

View File

@@ -3,17 +3,17 @@ package io.element.android.x.features.rageshake.logs
import android.content.Context
import android.util.Log
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.PrintWriter
import java.io.StringWriter
import java.util.logging.FileHandler
import java.util.logging.Level
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.

View File

@@ -1,7 +1,11 @@
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.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.airbnb.mvrx.compose.collectAsState
@@ -10,20 +14,34 @@ 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.PreferenceSwitch
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.RageshakeDetectionViewState
import io.element.android.x.element.resources.R as ElementR
@Composable
fun RageshakePreferenceCategory() {
RageshakePreferenceContent()
fun RageshakePreferences(
onOpenRageShake: () -> Unit = {},
) {
RageshakePreferencesContent(
onOpenRageShake = onOpenRageShake,
)
}
@Composable
fun RageshakePreferenceContent(
viewModel: RageshakeDetectionViewModel = mavericksViewModel()
fun RageshakePreferencesContent(
modifier: Modifier = Modifier,
viewModel: RageshakeDetectionViewModel = mavericksViewModel(),
onOpenRageShake: () -> Unit = {},
) {
val state: RageshakeDetectionViewState by viewModel.collectAsState()
Column(modifier = modifier) {
PreferenceCategory(title = stringResource(id = ElementR.string.send_bug_report)) {
PreferenceText(
title = stringResource(id = ElementR.string.send_bug_report),
icon = Icons.Default.BugReport,
onClick = onOpenRageShake
)
}
PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) {
if (state.isSupported) {
PreferenceSwitch(
@@ -31,23 +49,23 @@ fun RageshakePreferenceContent(
isChecked = state.isEnabled,
onCheckedChange = viewModel::onEnableClicked
)
if (state.isEnabled) {
PreferenceSlide(
title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold),
// summary = stringResource(id = ElementR.string.settings_rageshake_detection_threshold_summary),
value = state.sensitivity,
enabled = state.isEnabled,
steps = 3 /* 5 possible values - steps are in ]0, 1[ */,
onValueChange = viewModel::onSensitivityChange
)
}
} else {
PreferenceText(title = "Rageshaking is not supported by your device")
}
}
}
}
@Composable
@Preview
fun RageshakePreferenceCategoryPreview() {
RageshakePreferenceCategory()
fun RageshakePreferencePreview() {
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) {
shakeDetector?.setSensitivity(

View File

@@ -9,9 +9,9 @@ import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import io.element.android.x.core.bool.orTrue
import io.element.android.x.di.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
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.logs.VectorFileLogger
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.Dispatchers
import kotlinx.coroutines.flow.first
@@ -24,12 +30,6 @@ import okhttp3.Response
import org.json.JSONException
import org.json.JSONObject
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.
@@ -121,6 +121,7 @@ class BugReporter @Inject constructor(
/**
* Send a bug report.
*
* @param coroutineScope The coroutine scope
* @param reportType The report type (bug, suggestion, feedback)
* @param withDevicesLogs true to include the device log
* @param withCrashLogs true to include the crash logs

View File

@@ -21,13 +21,10 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.tooling.preview.Preview
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.mavericksViewModel
import io.element.android.x.core.compose.LogCompositions
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.features.roomlist.components.RoomListTopBar
import io.element.android.x.features.roomlist.components.RoomSummaryRow
@@ -42,17 +39,10 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
fun RoomListScreen(
viewModel: RoomListViewModel = mavericksViewModel(),
onSuccessLogout: () -> Unit = { },
onRoomClicked: (RoomId) -> Unit = { },
onOpenRageShake: () -> Unit = { },
onOpenSettings: () -> Unit = { },
) {
val logoutAction by viewModel.collectAsState(RoomListViewState::logoutAction)
val filter by viewModel.collectAsState(RoomListViewState::filter)
if (logoutAction is Success) {
onSuccessLogout()
return
}
LogCompositions(tag = "RoomListScreen", msg = "Root")
val roomSummaries by viewModel.collectAsState(RoomListViewState::rooms)
val matrixUser by viewModel.collectAsState(RoomListViewState::user)
@@ -60,10 +50,7 @@ fun RoomListScreen(
roomSummaries = roomSummaries().orEmpty().toImmutableList(),
matrixUser = matrixUser(),
onRoomClicked = onRoomClicked,
onLogoutClicked = viewModel::logout,
onOpenSettings = onOpenSettings,
onOpenRageShake = onOpenRageShake,
isLoginOut = logoutAction is Loading,
filter = filter,
onFilterChanged = viewModel::filterRoom,
onScrollOver = viewModel::updateVisibleRange
@@ -75,13 +62,10 @@ fun RoomListContent(
roomSummaries: ImmutableList<RoomListRoomSummary>,
matrixUser: MatrixUser?,
filter: String,
isLoginOut: Boolean,
modifier: Modifier = Modifier,
onRoomClicked: (RoomId) -> Unit = {},
onFilterChanged: (String) -> Unit = {},
onLogoutClicked: () -> Unit = {},
onOpenSettings: () -> Unit = {},
onOpenRageShake: () -> Unit = { },
onScrollOver: (IntRange) -> Unit = {},
) {
fun onRoomClicked(room: RoomListRoomSummary) {
@@ -118,9 +102,7 @@ fun RoomListContent(
matrixUser = matrixUser,
filter = filter,
onFilterChanged = onFilterChanged,
onLogoutClicked = onLogoutClicked,
onOpenSettings = onOpenSettings,
onOpenRageShake = onOpenRageShake,
scrollBehavior = scrollBehavior
)
},
@@ -142,9 +124,6 @@ fun RoomListContent(
}
}
)
if (isLoginOut) {
ProgressDialog(text = "Login out...")
}
}
private fun RoomListRoomSummary.contentType() = isPlaceholder
@@ -161,10 +140,8 @@ fun PreviewableRoomListContent() {
roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
onRoomClicked = {},
onLogoutClicked = {},
filter = "filter",
onFilterChanged = {},
isLoginOut = false,
onScrollOver = {}
)
}
@@ -178,10 +155,8 @@ fun PreviewableDarkRoomListContent() {
roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
onRoomClicked = {},
onLogoutClicked = {},
filter = "filter",
onFilterChanged = {},
isLoginOut = true,
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.room.RoomSummary
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
@@ -33,8 +32,7 @@ private const val extendedRangeSize = 40
class RoomListViewModel @AssistedInject constructor(
private val client: MatrixClient,
@Assisted initialState: RoomListViewState
) :
MavericksViewModel<RoomListViewState>(initialState) {
) : MavericksViewModel<RoomListViewState>(initialState) {
companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by daggerMavericksViewModelFactory()
@@ -44,17 +42,6 @@ class RoomListViewModel @AssistedInject constructor(
handleInit()
}
fun logout() {
viewModelScope.launch {
suspend {
delay(2000)
client.logout()
}.execute {
copy(logoutAction = it)
}
}
}
fun filterRoom(filter: String) {
setState {
copy(

View File

@@ -7,9 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
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.Logout
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
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.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.core.compose.textFieldState
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.element.resources.R as ElementR
@Composable
fun RoomListTopBar(
matrixUser: MatrixUser?,
filter: String,
onFilterChanged: (String) -> Unit,
onLogoutClicked: () -> Unit,
onOpenSettings: () -> Unit,
onOpenRageShake: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior
) {
LogCompositions(tag = "RoomListScreen", msg = "TopBar")
@@ -77,9 +70,7 @@ fun RoomListTopBar(
} else {
DefaultRoomListTopBar(
matrixUser = matrixUser,
onLogoutClicked = onLogoutClicked,
onOpenSettings = onOpenSettings,
onOpenRageShake = onOpenRageShake,
onSearchClicked = {
searchWidgetStateIsOpened = true
},
@@ -168,13 +159,10 @@ fun SearchRoomListTopBar(
@Composable
private fun DefaultRoomListTopBar(
matrixUser: MatrixUser?,
onLogoutClicked: () -> Unit,
onOpenRageShake: () -> Unit,
onOpenSettings: () -> Unit,
onSearchClicked: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior
) {
val openDialog = remember { mutableStateOf(false) }
MediumTopAppBar(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
@@ -197,16 +185,6 @@ private fun DefaultRoomListTopBar(
) {
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(
onClick = onOpenSettings
) {
@@ -215,19 +193,4 @@ private fun DefaultRoomListTopBar(
},
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 filter: String = "",
val canLoadMore: Boolean = false,
val logoutAction: Async<Unit> = Uninitialized,
val roomsById: Map<RoomId, RoomListRoomSummary> = emptyMap()
) : MavericksState

View File

@@ -1,8 +1,8 @@
package io.element.android.x.core.file
import timber.log.Timber
import java.io.File
import java.util.zip.GZIPOutputStream
import timber.log.Timber
/**
* 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.x.element.resources.R as ElementR
@Composable

View File

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

View File

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

View File

@@ -1,42 +1,58 @@
package io.element.android.x.designsystem.components.preferences
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
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.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
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
fun PreferenceSwitch(
title: String,
isChecked: Boolean,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
onCheckedChange: (Boolean) -> Unit = {},
) {
Box(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight),
.defaultMinSize(minHeight = preferenceMinHeight)
.clickable { onCheckedChange(!isChecked) },
contentAlignment = Alignment.CenterStart
) {
Row(
modifier = modifier
.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
PreferenceIcon(
icon = icon,
enabled = enabled
)
Text(
modifier = modifier
.weight(1f),
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyLarge,
color = enabled.toEnabledColor(),
text = title
)
Checkbox(checked = isChecked, onCheckedChange = onCheckedChange)
Checkbox(
checked = isChecked,
enabled = enabled,
onCheckedChange = onCheckedChange
)
}
}
}
@@ -46,6 +62,7 @@ fun PreferenceSwitch(
fun PreferenceSwitchPreview() {
PreferenceSwitch(
title = "Switch",
icon = Icons.Default.Announcement,
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.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.x.designsystem.components.preferences.components.PreferenceIcon
@Composable
fun PreferenceText(
title: String,
// TODO subtitle
modifier: Modifier = Modifier,
icon: ImageVector? = null,
onClick: () -> Unit = {},
) {
Box(
@@ -25,19 +31,25 @@ fun PreferenceText(
.clickable { onClick() },
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
PreferenceIcon(icon = icon)
Text(
modifier = Modifier
.fillMaxWidth(),
.weight(1f),
style = MaterialTheme.typography.bodyLarge,
text = title
)
}
}
}
@Composable
@Preview(showBackground = false)
fun PreferenceTextPreview() {
PreferenceText(
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(":features:onboarding")
include(":features:login")
include(":features:logout")
include(":features:roomlist")
include(":features:messages")
include(":features:rageshake")