Merge branch 'develop' into feature/fga/room_detail_factory_improvement

This commit is contained in:
ganfra 2023-07-04 10:22:52 +02:00
commit 91ad3dc834
204 changed files with 1838 additions and 822 deletions

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.21" />
<option name="version" value="1.8.22" />
</component>
</project>

View file

@ -2,12 +2,29 @@ appId: ${APP_ID}
---
- tapOn:
id: "home_screen-settings"
- assertVisible: "Rageshake to report bug"
- assertVisible: "Settings"
- takeScreenshot: build/maestro/600-Settings
- tapOn:
text: "Report bug"
index: 1
- assertVisible: "Describe the bug…"
text: "Analytics"
- assertVisible: "Share analytics data"
- back
- tapOn:
text: "Report bug"
- assertVisible: "Report a bug"
- back
- tapOn:
text: "About"
- assertVisible: "Copyright"
- assertVisible: "Acceptable use policy"
- assertVisible: "Privacy policy"
- back
- tapOn:
text: "Developer options"
- assertVisible: "Feature flags"
- back
- back
- runFlow: ../assertions/assertHomeDisplayed.yaml

View file

@ -77,6 +77,7 @@ object AppModule {
applicationId = BuildConfig.APPLICATION_ID,
lowPrivacyLoggingEnabled = false, // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
versionName = BuildConfig.VERSION_NAME,
versionCode = BuildConfig.VERSION_CODE,
gitRevision = "TODO", // BuildConfig.GIT_REVISION,
gitRevisionDate = "TODO", // BuildConfig.GIT_REVISION_DATE,
gitBranchName = "TODO", // BuildConfig.GIT_BRANCH_NAME,

View file

@ -31,7 +31,7 @@ import io.element.android.x.R
fun IconPreview(
modifier: Modifier = Modifier,
) {
Box {
Box(modifier = modifier) {
Image(painter = painterResource(id = R.mipmap.ic_launcher_background), contentDescription = null)
Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null)
}

View file

@ -69,7 +69,6 @@ import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -292,6 +291,10 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onOpenBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() }
}
override fun onVerifyClicked() {
backstack.push(NavTarget.VerifySession)
}
}
preferencesEntryPoint.nodeBuilder(this, buildContext)
.callback(callback)

View file

@ -225,8 +225,9 @@ koverMerged {
includes += "*Presenter"
excludes += "*Fake*Presenter"
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
// Too small presenter, cannot reach the threshold.
// Too small presenters, cannot reach the threshold.
excludes += "io.element.android.features.onboarding.impl.OnBoardingPresenter"
excludes += "io.element.android.features.preferences.impl.about.AboutPresenter"
}
bound {
minValue = 90

View file

@ -18,6 +18,7 @@ package io.element.android.features.analytics.api.preferences
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
@ -27,7 +28,6 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@ -43,20 +43,21 @@ fun AnalyticsPreferencesView(
state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
}
PreferenceCategory(title = stringResource(id = CommonStrings.screen_analytics_settings_share_data)) {
val firstPart = stringResource(id = CommonStrings.screen_analytics_settings_help_us_improve, state.applicationName)
val secondPart = buildAnnotatedStringWithColoredPart(
CommonStrings.screen_analytics_settings_read_terms,
CommonStrings.screen_analytics_settings_read_terms_content_link
)
val title = "$firstPart\n\n$secondPart"
val firstPart = stringResource(id = CommonStrings.screen_analytics_settings_help_us_improve, state.applicationName)
val secondPart = buildAnnotatedStringWithColoredPart(
CommonStrings.screen_analytics_settings_read_terms,
CommonStrings.screen_analytics_settings_read_terms_content_link
)
val subtitle = "$firstPart\n\n$secondPart"
PreferenceSwitch(
title = title,
isChecked = state.isEnabled,
onCheckedChange = ::onEnabledChanged
)
}
PreferenceSwitch(
modifier = modifier,
title = stringResource(id = CommonStrings.screen_analytics_settings_share_data),
subtitle = subtitle,
isChecked = state.isEnabled,
onCheckedChange = ::onEnabledChanged,
switchAlignment = Alignment.Top,
)
}
@Composable

View file

@ -174,7 +174,7 @@ private fun AnalyticsOptInContentRow(
.padding(2.dp),
imageVector = Icons.Rounded.Check,
contentDescription = null,
tint = ElementTheme.colors.iconSuccessPrimary,
tint = ElementTheme.colors.textActionAccent,
)
Text(
modifier = Modifier.padding(start = 16.dp),

View file

@ -21,9 +21,8 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.impl.preferences.DefaultAnalyticsPreferencesPresenter
import io.element.android.features.analytics.test.A_BUILD_META
import io.element.android.features.analytics.test.FakeAnalyticsService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -33,7 +32,7 @@ class AnalyticsOptInPresenterTest {
fun `present - enable`() = runTest {
val analyticsService = FakeAnalyticsService(isEnabled = false)
val presenter = AnalyticsOptInPresenter(
A_BUILD_META,
aBuildMeta(),
analyticsService
)
moleculeFlow(RecompositionClock.Immediate) {
@ -51,7 +50,7 @@ class AnalyticsOptInPresenterTest {
fun `present - not now`() = runTest {
val analyticsService = FakeAnalyticsService(isEnabled = false)
val presenter = AnalyticsOptInPresenter(
A_BUILD_META,
aBuildMeta(),
analyticsService
)
moleculeFlow(RecompositionClock.Immediate) {

View file

@ -21,8 +21,8 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.test.A_BUILD_META
import io.element.android.features.analytics.test.FakeAnalyticsService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -31,7 +31,7 @@ class AnalyticsPreferencesPresenterTest {
fun `present - initial state available`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
A_BUILD_META
aBuildMeta()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -46,7 +46,7 @@ class AnalyticsPreferencesPresenterTest {
fun `present - initial state not available`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = false, didAskUserConsent = false),
A_BUILD_META
aBuildMeta()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -60,7 +60,7 @@ class AnalyticsPreferencesPresenterTest {
fun `present - enable and disable`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
A_BUILD_META
aBuildMeta()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()

View file

@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.usersearch.test.FakeUserRepository
import kotlinx.collections.immutable.persistentListOf
@ -183,18 +184,3 @@ class CreateRoomRootPresenterTests {
}
}
}
private fun aBuildMeta() =
BuildMeta(
buildType = BuildType.DEBUG,
isDebuggable = true,
applicationId = "",
applicationName = "An Application",
lowPrivacyLoggingEnabled = true,
versionName = "",
gitRevision = "",
gitBranchName = "",
gitRevisionDate = "",
flavorDescription = "",
flavorShortDescription = "",
)

View file

@ -27,11 +27,9 @@ import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun LogoutPreferenceView(
@ -81,13 +79,11 @@ fun LogoutPreferenceView(
fun LogoutPreferenceContent(
onClick: () -> Unit = {},
) {
PreferenceCategory(title = stringResource(id = CommonStrings.settings_title_general)) {
PreferenceText(
title = stringResource(id = R.string.screen_signout_preference_item),
icon = Icons.Default.Logout,
onClick = onClick
)
}
PreferenceText(
title = stringResource(id = R.string.screen_signout_preference_item),
icon = Icons.Filled.Logout,
onClick = onClick
)
}
@Preview

View file

@ -135,7 +135,8 @@ class MessageComposerPresenter @Inject constructor(
Composer(
inThread = false,
isEditing = composerMode.value is MessageComposerMode.Edit,
isReply = composerMode.value is MessageComposerMode.Reply
isReply = composerMode.value is MessageComposerMode.Reply,
isLocation = false,
)
)
}

View file

@ -59,6 +59,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.mediapickers.test.FakePickerProvider
@ -566,19 +567,7 @@ class MessagesPresenterTest {
timelineItemsFactory = aTimelineItemsFactory(),
room = matrixRoom,
)
val buildMeta = BuildMeta(
buildType = BuildType.DEBUG,
isDebuggable = true,
applicationId = "",
applicationName = "",
lowPrivacyLoggingEnabled = true,
versionName = "",
gitRevision = "",
gitBranchName = "",
gitRevisionDate = "",
flavorDescription = "",
flavorShortDescription = "",
)
val buildMeta = aBuildMeta()
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
val customReactionPresenter = CustomReactionPresenter()
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)

View file

@ -25,13 +25,11 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.core.aBuildMeta
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -74,7 +72,6 @@ class ActionListPresenterTest {
}
}
@Test
fun `present - compute for message from others redacted`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
@ -231,31 +228,5 @@ class ActionListPresenterTest {
}
}
private fun aBuildMeta(
buildType: BuildType = BuildType.DEBUG,
isDebuggable: Boolean = true,
applicationName: String = "",
applicationId: String = "",
lowPrivacyLoggingEnabled: Boolean = true,
versionName: String = "",
gitRevision: String = "",
gitRevisionDate: String = "",
gitBranchName: String = "",
flavorDescription: String = "",
flavorShortDescription: String = "",
) = BuildMeta(
buildType,
isDebuggable,
applicationName,
applicationId,
lowPrivacyLoggingEnabled,
versionName,
gitRevision,
gitRevisionDate,
gitBranchName,
flavorDescription,
flavorShortDescription
)
private fun anActionListPresenter(isBuildDebuggable: Boolean) = ActionListPresenter(buildMeta = aBuildMeta(isDebuggable = isBuildDebuggable))

View file

@ -22,7 +22,9 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.QrCode
@ -31,7 +33,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@ -49,8 +50,14 @@ import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
// Ref: https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=133-5427&t=5SHVppfYzjvkEywR-0
// Refs:
// FTUE:
// - https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=133-5427&t=5SHVppfYzjvkEywR-0
// ElementX:
// - https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=1816-97419
@Composable
fun OnBoardingView(
state: OnBoardingState,
@ -61,6 +68,9 @@ fun OnBoardingView(
) {
OnBoardingPage(
modifier = modifier,
content = {
OnBoardingContent()
},
footer = {
OnBoardingButtons(
state = state,
@ -69,42 +79,61 @@ fun OnBoardingView(
onCreateAccount = onCreateAccount,
)
}
) {
OnBoardingContent()
}
)
}
@Composable
private fun OnBoardingContent(modifier: Modifier = Modifier) {
// Note: having a night variant of R.drawable.onboarding_icon in the folder `drawable-night` is working
// at runtime, but is not in Android Studio Preview. So I prefer to handle this manually.
val isLight = ElementTheme.colors.isLight
val iconDrawableRes = if (isLight) R.drawable.onboarding_icon_light else R.drawable.onboarding_icon_dark
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = BiasAlignment(
horizontalBias = 0f,
verticalBias = -0.2f
)
) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = CenterHorizontally,
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = BiasAlignment(
horizontalBias = 0f,
verticalBias = -0.4f
)
) {
// Dark and light icon does not have the same size, add padding to the smaller one
val imagePadding = if (isLight) 28.dp else 0.dp
Image(
painter = painterResource(id = R.drawable.element_logo),
modifier = Modifier
.size(278.dp)
.padding(imagePadding),
painter = painterResource(id = iconDrawableRes),
contentDescription = null,
)
Image(
modifier = Modifier.padding(top = 14.dp),
painter = painterResource(id = R.drawable.element),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
contentDescription = null,
)
Text(
modifier = Modifier.padding(top = 24.dp),
text = stringResource(id = R.string.screen_onboarding_subtitle),
color = MaterialTheme.colorScheme.secondary,
fontSize = 20.sp,
textAlign = TextAlign.Center
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = BiasAlignment(
horizontalBias = 0f,
verticalBias = 0.6f
)
) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = CenterHorizontally,
) {
Text(
text = stringResource(id = R.string.screen_onboarding_welcome_title),
color = ElementTheme.materialColors.primary,
style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = R.string.screen_onboarding_welcome_message),
color = ElementTheme.materialColors.secondary,
style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = 17.sp),
textAlign = TextAlign.Center
)
}
}
}
}
@ -118,6 +147,11 @@ private fun OnBoardingButtons(
modifier: Modifier = Modifier,
) {
ButtonColumnMolecule(modifier = modifier) {
val signInButtonStringRes = if (state.canLoginWithQrCode || state.canCreateAccount) {
R.string.screen_onboarding_sign_in_manually
} else {
CommonStrings.action_continue
}
if (state.canLoginWithQrCode) {
Button(
onClick = onSignInWithQrCode,
@ -130,7 +164,10 @@ private fun OnBoardingButtons(
tint = MaterialTheme.colorScheme.onPrimary
)
Spacer(Modifier.width(14.dp))
Text(text = stringResource(id = R.string.screen_onboarding_sign_in_with_qr_code))
Text(
text = stringResource(id = R.string.screen_onboarding_sign_in_with_qr_code),
style = ElementTheme.typography.fontBodyLgMedium,
)
}
}
Button(
@ -140,7 +177,10 @@ private fun OnBoardingButtons(
.fillMaxWidth()
.testTag(TestTags.onBoardingSignIn)
) {
Text(text = stringResource(id = R.string.screen_onboarding_sign_in_manually))
Text(
text = stringResource(id = signInButtonStringRes),
style = ElementTheme.typography.fontBodyLgMedium,
)
}
if (state.canCreateAccount) {
OutlinedButton(
@ -149,9 +189,13 @@ private fun OnBoardingButtons(
modifier = Modifier
.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.screen_onboarding_sign_up))
Text(
text = stringResource(id = R.string.screen_onboarding_sign_up),
style = ElementTheme.typography.fontBodyLgMedium,
)
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}

View file

@ -1,27 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="160dp"
android:height="34dp"
android:viewportWidth="160"
android:viewportHeight="34">
<path
android:pathData="M22.18,23.71H5.07C5.27,25.51 5.92,26.94 7.02,28.01C8.12,29.05 9.56,29.57 11.35,29.57C12.53,29.57 13.6,29.28 14.56,28.71C15.51,28.13 16.19,27.35 16.59,26.36H21.79C21.1,28.65 19.8,30.5 17.89,31.92C16.01,33.31 13.79,34 11.22,34C7.87,34 5.16,32.89 3.08,30.66C1.03,28.43 0,25.61 0,22.2C0,18.87 1.04,16.08 3.12,13.82C5.2,11.56 7.88,10.43 11.18,10.43C14.47,10.43 17.13,11.55 19.15,13.78C21.2,15.97 22.22,18.75 22.22,22.11L22.18,23.71ZM11.18,14.64C9.56,14.64 8.22,15.12 7.15,16.08C6.08,17.03 5.42,18.3 5.16,19.9H17.11C16.88,18.3 16.25,17.03 15.21,16.08C14.17,15.12 12.82,14.64 11.18,14.64Z"
android:fillColor="#1B1D22"/>
<path
android:pathData="M25.77,26.75V0.93H30.93V26.84C30.93,28 31.56,28.58 32.83,28.58L33.74,28.53V33.44C33.25,33.52 32.73,33.57 32.18,33.57C29.96,33.57 28.33,33 27.29,31.87C26.28,30.75 25.77,29.04 25.77,26.75Z"
android:fillColor="#1B1D22"/>
<path
android:pathData="M57.75,23.71H40.64C40.84,25.51 41.49,26.94 42.59,28.01C43.69,29.05 45.13,29.57 46.92,29.57C48.11,29.57 49.18,29.28 50.13,28.71C51.08,28.13 51.76,27.35 52.16,26.36H57.36C56.67,28.65 55.37,30.5 53.46,31.92C51.59,33.31 49.36,34 46.79,34C43.44,34 40.73,32.89 38.65,30.66C36.6,28.43 35.57,25.61 35.57,22.2C35.57,18.87 36.61,16.08 38.69,13.82C40.77,11.56 43.46,10.43 46.75,10.43C50.04,10.43 52.7,11.55 54.72,13.78C56.77,15.97 57.8,18.75 57.8,22.11L57.75,23.71ZM46.75,14.64C45.13,14.64 43.79,15.12 42.72,16.08C41.65,17.03 40.99,18.3 40.73,19.9H52.68C52.45,18.3 51.82,17.03 50.78,16.08C49.74,15.12 48.4,14.64 46.75,14.64Z"
android:fillColor="#1B1D22"/>
<path
android:pathData="M81.01,20.55V33.48H75.86V19.98C75.86,16.57 74.44,14.86 71.61,14.86C70.08,14.86 68.85,15.35 67.93,16.34C67.03,17.32 66.59,18.67 66.59,20.37V33.48H61.43V10.95H66.2V13.95C66.75,12.94 67.58,12.1 68.71,11.43C69.84,10.77 71.24,10.43 72.91,10.43C76.03,10.43 78.28,11.62 79.67,13.99C81.58,11.62 84.12,10.43 87.29,10.43C89.92,10.43 91.94,11.26 93.36,12.91C94.77,14.53 95.48,16.67 95.48,19.33V33.48H90.33V19.98C90.33,16.57 88.91,14.86 86.08,14.86C84.52,14.86 83.28,15.37 82.36,16.38C81.46,17.36 81.01,18.75 81.01,20.55Z"
android:fillColor="#1B1D22"/>
<path
android:pathData="M121.12,23.71H104.01C104.21,25.51 104.86,26.94 105.96,28.01C107.06,29.05 108.5,29.57 110.29,29.57C111.47,29.57 112.54,29.28 113.5,28.71C114.45,28.13 115.13,27.35 115.53,26.36H120.73C120.04,28.65 118.74,30.5 116.83,31.92C114.96,33.31 112.73,34 110.16,34C106.81,34 104.1,32.89 102.02,30.66C99.97,28.43 98.94,25.61 98.94,22.2C98.94,18.87 99.98,16.08 102.06,13.82C104.14,11.56 106.82,10.43 110.12,10.43C113.41,10.43 116.07,11.55 118.09,13.78C120.14,15.97 121.16,18.75 121.16,22.11L121.12,23.71ZM110.12,14.64C108.5,14.64 107.16,15.12 106.09,16.08C105.02,17.03 104.36,18.3 104.1,19.9H116.05C115.82,18.3 115.19,17.03 114.15,16.08C113.11,15.12 111.76,14.64 110.12,14.64Z"
android:fillColor="#1B1D22"/>
<path
android:pathData="M129.56,10.95V13.95C130.08,12.97 130.94,12.14 132.12,11.48C133.33,10.78 134.79,10.43 136.5,10.43C139.15,10.43 141.2,11.24 142.65,12.86C144.12,14.48 144.86,16.64 144.86,19.33V33.48H139.7V19.98C139.7,18.39 139.33,17.15 138.57,16.25C137.85,15.32 136.74,14.86 135.24,14.86C133.59,14.86 132.29,15.35 131.34,16.34C130.42,17.32 129.95,18.68 129.95,20.42V33.48H124.8V10.95H129.56Z"
android:fillColor="#1B1D22"/>
<path
android:pathData="M159.91,28.88V33.35C159.28,33.52 158.38,33.61 157.23,33.61C152.84,33.61 150.64,31.4 150.64,26.97V15.08H147.22V10.95H150.64V5.1H155.8V10.95H160V15.08H155.8V26.45C155.8,28.21 156.63,29.1 158.31,29.1L159.91,28.88Z"
android:fillColor="#1B1D22"/>
</vector>

View file

@ -1,26 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="49dp"
android:viewportWidth="50"
android:viewportHeight="49">
<path
android:pathData="M24.8,48.608C38.199,48.608 49.061,37.726 49.061,24.304C49.061,10.881 38.199,0 24.8,0C11.401,0 0.54,10.881 0.54,24.304C0.54,37.726 11.401,48.608 24.8,48.608Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M20.365,11.324C20.365,10.343 21.16,9.548 22.142,9.548C28.793,9.548 34.185,14.938 34.185,21.587C34.185,22.568 33.389,23.363 32.408,23.363C31.426,23.363 30.631,22.568 30.631,21.587C30.631,16.9 26.83,13.1 22.142,13.1C21.16,13.1 20.365,12.305 20.365,11.324Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M37.753,19.81C38.735,19.81 39.53,20.606 39.53,21.587C39.53,28.236 34.138,33.626 27.487,33.626C26.506,33.626 25.71,32.831 25.71,31.85C25.71,30.869 26.506,30.073 27.487,30.073C32.176,30.073 35.976,26.274 35.976,21.587C35.976,20.606 36.772,19.81 37.753,19.81Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M29.264,37.283C29.264,38.264 28.468,39.06 27.487,39.06C20.836,39.06 15.444,33.669 15.444,27.02C15.444,26.039 16.24,25.244 17.221,25.244C18.202,25.244 18.998,26.039 18.998,27.02C18.998,31.708 22.799,35.507 27.487,35.507C28.468,35.507 29.264,36.302 29.264,37.283Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M11.847,28.797C10.866,28.797 10.071,28.002 10.071,27.021C10.071,20.372 15.462,14.981 22.113,14.981C23.095,14.981 23.89,15.777 23.89,16.758C23.89,17.739 23.095,18.534 22.113,18.534C17.425,18.534 13.624,22.333 13.624,27.021C13.624,28.002 12.829,28.797 11.847,28.797Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -32,5 +32,6 @@ interface PreferencesEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onOpenBugReport()
fun onVerifyClicked()
}
}

View file

@ -46,9 +46,11 @@ dependencies {
implementation(projects.features.analytics.api)
implementation(projects.libraries.matrixui)
implementation(projects.features.logout.api)
implementation(projects.services.toolbox.api)
implementation(libs.datetime)
implementation(libs.accompanist.placeholder)
implementation(libs.coil.compose)
implementation(libs.androidx.browser)
api(projects.features.preferences.api)
ksp(libs.showkase.processor)

View file

@ -30,6 +30,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.preferences.impl.about.AboutNode
import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode
import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode
import io.element.android.features.preferences.impl.root.PreferencesRootNode
import io.element.android.libraries.architecture.BackstackNode
@ -57,6 +59,12 @@ class PreferencesFlowNode @AssistedInject constructor(
@Parcelize
object DeveloperSettings : NavTarget
@Parcelize
object AnalyticsSettings : NavTarget
@Parcelize
object About : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -67,6 +75,18 @@ class PreferencesFlowNode @AssistedInject constructor(
plugins<PreferencesEntryPoint.Callback>().forEach { it.onOpenBugReport() }
}
override fun onVerifyClicked() {
plugins<PreferencesEntryPoint.Callback>().forEach { it.onVerifyClicked() }
}
override fun onOpenAnalytics() {
backstack.push(NavTarget.AnalyticsSettings)
}
override fun onOpenAbout() {
backstack.push(NavTarget.About)
}
override fun onOpenDeveloperSettings() {
backstack.push(NavTarget.DeveloperSettings)
}
@ -76,6 +96,12 @@ class PreferencesFlowNode @AssistedInject constructor(
NavTarget.DeveloperSettings -> {
createNode<DeveloperSettingsNode>(buildContext)
}
NavTarget.About -> {
createNode<AboutNode>(buildContext)
}
NavTarget.AnalyticsSettings -> {
createNode<AnalyticsSettingsNode>(buildContext)
}
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.about
import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.theme.ElementTheme
@ContributesNode(SessionScope::class)
class AboutNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: AboutPresenter,
) : Node(buildContext, plugins = plugins) {
private fun onElementLegalClicked(
activity: Activity,
darkTheme: Boolean,
elementLegal: ElementLegal,
) {
activity.openUrlInChromeCustomTab(null, darkTheme, elementLegal.url)
}
@Composable
override fun View(modifier: Modifier) {
val activity = LocalContext.current as Activity
val isDark = ElementTheme.isLightTheme.not()
val state = presenter.present()
AboutView(
state = state,
onBackPressed = ::navigateUp,
onElementLegalClicked = { elementLegal ->
onElementLegalClicked(activity, isDark, elementLegal)
},
modifier = modifier
)
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.about
import androidx.compose.runtime.Composable
import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject
class AboutPresenter @Inject constructor() : Presenter<AboutState> {
@Composable
override fun present(): AboutState {
return AboutState(
elementLegals = getAllLegals(),
)
}
}

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.about
// Do not use default value, so no member get forgotten in the presenters.
data class AboutState(
val elementLegals: List<ElementLegal>,
)

View file

@ -14,22 +14,17 @@
* limitations under the License.
*/
package io.element.android.features.analytics.test
package io.element.android.features.preferences.impl.about
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
val A_BUILD_META = BuildMeta(
isDebuggable = true,
buildType = BuildType.DEBUG,
applicationName = "Element X test",
applicationId = "",
lowPrivacyLoggingEnabled = false,
versionName = "",
gitRevision = "",
gitRevisionDate = "",
gitBranchName = "",
flavorDescription = "",
flavorShortDescription = "",
open class AboutStateProvider : PreviewParameterProvider<AboutState> {
override val values: Sequence<AboutState>
get() = sequenceOf(
aAboutState(),
)
}
fun aAboutState() = AboutState(
elementLegals = getAllLegals(),
)

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.about
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
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.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun AboutView(
state: AboutState,
onElementLegalClicked: (ElementLegal) -> Unit,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferenceView(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.common_about)
) {
state.elementLegals.forEach { elementLegal ->
PreferenceText(
title = stringResource(id = elementLegal.titleRes),
onClick = { onElementLegalClicked(elementLegal) }
)
}
}
}
@Preview
@Composable
fun AboutViewLightPreview(@PreviewParameter(AboutStateProvider::class) state: AboutState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
fun AboutViewDarkPreview(@PreviewParameter(AboutStateProvider::class) state: AboutState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: AboutState) {
AboutView(
state = state,
onElementLegalClicked = {},
onBackPressed = {},
)
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.about
import androidx.annotation.StringRes
import io.element.android.libraries.ui.strings.CommonStrings
private const val CopyrightUrl = "https://element.io/copyright"
private const val UsePolicyUrl = "https://element.io/acceptable-use-policy-terms"
private const val PrivacyUrl = "https://element.io/privacy"
sealed class ElementLegal(
@StringRes val titleRes: Int,
val url: String,
) {
object Copyright : ElementLegal(CommonStrings.common_copyright, CopyrightUrl)
object AcceptableUsePolicy : ElementLegal(CommonStrings.common_acceptable_use_policy, UsePolicyUrl)
object PrivacyPolicy : ElementLegal(CommonStrings.common_privacy_policy, PrivacyUrl)
}
fun getAllLegals(): List<ElementLegal> {
return listOf(
ElementLegal.Copyright,
ElementLegal.AcceptableUsePolicy,
ElementLegal.PrivacyPolicy,
)
}

View file

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

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.analytics
import androidx.compose.runtime.Composable
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter
import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject
class AnalyticsSettingsPresenter @Inject constructor(
private val analyticsPresenter: AnalyticsPreferencesPresenter,
) : Presenter<AnalyticsSettingsState> {
@Composable
override fun present(): AnalyticsSettingsState {
val analyticsState = analyticsPresenter.present()
return AnalyticsSettingsState(
analyticsState = analyticsState,
)
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.analytics
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
// Do not use default value, so no member get forgotten in the presenters.
data class AnalyticsSettingsState(
val analyticsState: AnalyticsPreferencesState,
)

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.analytics
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.analytics.api.preferences.aAnalyticsPreferencesState
open class AnalyticsSettingsStateProvider : PreviewParameterProvider<AnalyticsSettingsState> {
override val values: Sequence<AnalyticsSettingsState>
get() = sequenceOf(
aAnalyticsSettingsState(),
)
}
fun aAnalyticsSettingsState() = AnalyticsSettingsState(
analyticsState = aAnalyticsPreferencesState(),
)

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.analytics
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
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.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun AnalyticsSettingsView(
state: AnalyticsSettingsState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferenceView(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.common_analytics)
) {
AnalyticsPreferencesView(
state = state.analyticsState,
)
}
}
@Preview
@Composable
fun AnalyticsSettingsViewLightPreview(@PreviewParameter(AnalyticsSettingsStateProvider::class) state: AnalyticsSettingsState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
fun AnalyticsSettingsViewDarkPreview(@PreviewParameter(AnalyticsSettingsStateProvider::class) state: AnalyticsSettingsState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: AnalyticsSettingsState) {
AnalyticsSettingsView(
state = state,
onBackPressed = {},
)
}

View file

@ -27,6 +27,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshots.SnapshotStateMap
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
@ -44,10 +45,12 @@ class DeveloperSettingsPresenter @Inject constructor(
private val featureFlagService: FeatureFlagService,
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
private val clearCacheUseCase: ClearCacheUseCase,
private val rageshakePresenter: RageshakePreferencesPresenter,
) : Presenter<DeveloperSettingsState> {
@Composable
override fun present(): DeveloperSettingsState {
val rageshakeState = rageshakePresenter.present()
val features = remember {
mutableStateMapOf<String, Feature>()
@ -90,6 +93,7 @@ class DeveloperSettingsPresenter @Inject constructor(
features = featureUiModels.toImmutableList(),
cacheSize = cacheSize.value,
clearCacheAction = clearCacheAction.value,
rageshakeState = rageshakeState,
eventSink = ::handleEvents
)
}

View file

@ -16,6 +16,7 @@
package io.element.android.features.preferences.impl.developer
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import kotlinx.collections.immutable.ImmutableList
@ -23,6 +24,7 @@ import kotlinx.collections.immutable.ImmutableList
data class DeveloperSettingsState constructor(
val features: ImmutableList<FeatureUiModel>,
val cacheSize: Async<String>,
val rageshakeState: RageshakePreferencesState,
val clearCacheAction: Async<Unit>,
val eventSink: (DeveloperSettingsEvents) -> Unit
)

View file

@ -17,6 +17,7 @@
package io.element.android.features.preferences.impl.developer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList
@ -30,6 +31,7 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
fun aDeveloperSettingsState() = DeveloperSettingsState(
features = aFeatureUiModelList(),
rageshakeState = aRageshakePreferencesState(),
cacheSize = Async.Success("1.2 MB"),
clearCacheAction = Async.Uninitialized,
eventSink = {}

View file

@ -16,13 +16,12 @@
package io.element.android.features.preferences.impl.developer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
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
@ -54,11 +53,13 @@ fun DeveloperSettingsView(
onClick = onOpenShowkase
)
}
RageshakePreferencesView(
state = state.rageshakeState,
)
val cache = state.cacheSize
PreferenceCategory(title = "Cache") {
PreferenceCategory(title = "Cache", showDivider = false) {
PreferenceText(
title = "Clear cache",
icon = Icons.Default.Delete,
currentValue = cache.dataOrNull(),
loadingCurrentValue = state.cacheSize.isLoading() || state.clearCacheAction.isLoading(),
onClick = {

View file

@ -36,6 +36,9 @@ class PreferencesRootNode @AssistedInject constructor(
interface Callback : Plugin {
fun onOpenBugReport()
fun onVerifyClicked()
fun onOpenAnalytics()
fun onOpenAbout()
fun onOpenDeveloperSettings()
}
@ -43,10 +46,22 @@ class PreferencesRootNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onOpenBugReport() }
}
private fun onVerifyClicked() {
plugins<Callback>().forEach { it.onVerifyClicked() }
}
private fun onOpenDeveloperSettings() {
plugins<Callback>().forEach { it.onOpenDeveloperSettings() }
}
private fun onOpenAnalytics() {
plugins<Callback>().forEach { it.onOpenAnalytics() }
}
private fun onOpenAbout() {
plugins<Callback>().forEach { it.onOpenAbout() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -55,8 +70,10 @@ class PreferencesRootNode @AssistedInject constructor(
modifier = modifier,
onBackPressed = this::navigateUp,
onOpenRageShake = this::onOpenBugReport,
onOpenAnalytics = this::onOpenAnalytics,
onOpenAbout = this::onOpenAbout,
onVerifyClicked = this::onVerifyClicked,
onOpenDeveloperSettings = this::onOpenDeveloperSettings
)
}
}

View file

@ -17,33 +17,61 @@
package io.element.android.features.preferences.impl.root
import androidx.compose.runtime.Composable
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.logout.api.LogoutPreferencePresenter
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.user.getCurrentUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class PreferencesRootPresenter @Inject constructor(
private val logoutPresenter: LogoutPreferencePresenter,
private val rageshakePresenter: RageshakePreferencesPresenter,
private val analyticsPresenter: AnalyticsPreferencesPresenter,
private val matrixClient: MatrixClient,
private val sessionVerificationService: SessionVerificationService,
private val buildType: BuildType,
private val versionFormatter: VersionFormatter,
) : Presenter<PreferencesRootState> {
@Composable
override fun present(): PreferencesRootState {
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
mutableStateOf(null)
}
LaunchedEffect(Unit) {
initialLoad(matrixUser)
}
// Session verification status (unknown, not verified, verified)
val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
val sessionIsNotVerified by remember {
derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified }
}
val logoutState = logoutPresenter.present()
val rageshakeState = rageshakePresenter.present()
val analyticsState = analyticsPresenter.present()
val showDeveloperSettings = buildType != BuildType.RELEASE
return PreferencesRootState(
logoutState = logoutState,
rageshakeState = rageshakeState,
analyticsState = analyticsState,
myUser = Async.Uninitialized,
myUser = matrixUser.value,
version = versionFormatter.get(),
showCompleteVerification = sessionIsNotVerified,
showDeveloperSettings = showDeveloperSettings
)
}
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
matrixUser.value = matrixClient.getCurrentUser()
}
}

View file

@ -16,16 +16,13 @@
package io.element.android.features.preferences.impl.root
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
import io.element.android.features.logout.api.LogoutPreferenceState
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.user.MatrixUser
data class PreferencesRootState(
val logoutState: LogoutPreferenceState,
val rageshakeState: RageshakePreferencesState,
val analyticsState: AnalyticsPreferencesState,
val myUser: Async<MatrixUser>,
val myUser: MatrixUser?,
val version: String,
val showCompleteVerification: Boolean,
val showDeveloperSettings: Boolean
)

View file

@ -16,15 +16,12 @@
package io.element.android.features.preferences.impl.root
import io.element.android.features.analytics.api.preferences.aAnalyticsPreferencesState
import io.element.android.features.logout.api.aLogoutPreferenceState
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
import io.element.android.libraries.architecture.Async
fun aPreferencesRootState() = PreferencesRootState(
logoutState = aLogoutPreferenceState(),
rageshakeState = aRageshakePreferencesState(),
analyticsState = aAnalyticsPreferencesState(),
myUser = Async.Uninitialized,
myUser = null,
version = "Version 1.1 (1)",
showCompleteVerification = true,
showDeveloperSettings = true
)

View file

@ -16,36 +16,45 @@
package io.element.android.features.preferences.impl.root
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DeveloperMode
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.VerifiedUser
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
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.features.analytics.api.preferences.AnalyticsPreferencesView
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
import io.element.android.libraries.architecture.Async
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.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.LargeHeightPreview
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.designsystem.theme.components.Text
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
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun PreferencesRootView(
state: PreferencesRootState,
onBackPressed: () -> Unit,
onVerifyClicked: () -> Unit,
onOpenAnalytics: () -> Unit,
onOpenRageShake: () -> Unit,
onOpenAbout: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
onOpenRageShake: () -> Unit = {},
onOpenDeveloperSettings: () -> Unit = {},
) {
// TODO Hierarchy!
// Include pref from other modules
PreferenceView(
modifier = modifier,
@ -53,31 +62,55 @@ fun PreferencesRootView(
title = stringResource(id = CommonStrings.common_settings)
) {
UserPreferences(state.myUser)
AnalyticsPreferencesView(
state = state.analyticsState,
if (state.showCompleteVerification) {
PreferenceText(
title = stringResource(id = CommonStrings.action_complete_verification),
icon = Icons.Outlined.VerifiedUser,
onClick = onVerifyClicked,
)
Divider()
}
PreferenceText(
title = stringResource(id = CommonStrings.common_analytics),
icon = Icons.Outlined.InsertChart,
onClick = onOpenAnalytics,
)
RageshakePreferencesView(
state = state.rageshakeState,
onOpenRageshake = onOpenRageShake,
PreferenceText(
title = stringResource(id = CommonStrings.action_report_bug),
icon = Icons.Outlined.BugReport,
onClick = onOpenRageShake
)
LogoutPreferenceView(
state = state.logoutState,
PreferenceText(
title = stringResource(id = CommonStrings.common_about),
icon = Icons.Outlined.Help,
onClick = onOpenAbout,
)
if (state.showDeveloperSettings) {
DeveloperPreferencesView(onOpenDeveloperSettings)
}
Divider()
LogoutPreferenceView(
state = state.logoutState,
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 40.dp, bottom = 24.dp),
textAlign = TextAlign.Center,
text = state.version,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.materialColors.secondary,
)
}
}
@Composable
fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
PreferenceCategory(title = stringResource(id = CommonStrings.common_developer_options)) {
PreferenceText(
title = stringResource(id = CommonStrings.common_developer_options),
icon = Icons.Default.DeveloperMode,
onClick = onOpenDeveloperSettings
)
}
PreferenceText(
title = stringResource(id = CommonStrings.common_developer_options),
icon = Icons.Outlined.DeveloperMode,
onClick = onOpenDeveloperSettings
)
}
@LargeHeightPreview
@ -92,5 +125,13 @@ fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class)
@Composable
private fun ContentToPreview(matrixUser: MatrixUser) {
PreferencesRootView(aPreferencesRootState().copy(myUser = Async.Success(matrixUser)))
PreferencesRootView(
state = aPreferencesRootState().copy(myUser = matrixUser),
onBackPressed = {},
onOpenAnalytics = {},
onOpenRageShake = {},
onOpenDeveloperSettings = {},
onOpenAbout = {},
onVerifyClicked = {},
)
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.root
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.api.strings.StringProvider
import javax.inject.Inject
interface VersionFormatter {
fun get(): String
}
@ContributesBinding(AppScope::class)
class DefaultVersionFormatter @Inject constructor(
private val stringProvider: StringProvider,
private val buildMeta: BuildMeta,
) : VersionFormatter {
override fun get(): String {
return stringProvider.getString(
CommonStrings.settings_version_number,
buildMeta.versionName,
buildMeta.versionCode.toString()
)
}
}
class FakeVersionFormatter : VersionFormatter {
override fun get(): String {
return "A Version"
}
}

View file

@ -16,14 +16,10 @@
package io.element.android.features.preferences.impl.user
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.matrix.api.user.MatrixUser
@ -32,16 +28,13 @@ import io.element.android.libraries.matrix.ui.components.MatrixUserWithNullProvi
@Composable
fun UserPreferences(
user: Async<MatrixUser>,
user: MatrixUser?,
modifier: Modifier = Modifier,
) {
when (val userData = user.dataOrNull()) {
null -> Spacer(modifier = modifier.height(1.dp))
else -> MatrixUserHeader(
modifier = modifier,
matrixUser = userData
)
}
MatrixUserHeader(
modifier = modifier,
matrixUser = user
)
}
@Preview
@ -56,9 +49,5 @@ internal fun UserPreferencesDarkPreview(@PreviewParameter(MatrixUserWithNullProv
@Composable
private fun ContentToPreview(matrixUser: MatrixUser?) {
if (matrixUser == null) {
UserPreferences(Async.Uninitialized)
} else {
UserPreferences(Async.Success(matrixUser))
}
UserPreferences(matrixUser)
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.about
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
class AboutPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = AboutPresenter()
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.elementLegals).isEqualTo(getAllLegals())
}
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.analytics
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.impl.preferences.DefaultAnalyticsPreferencesPresenter
import io.element.android.features.analytics.test.FakeAnalyticsService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import kotlinx.coroutines.test.runTest
import org.junit.Test
class AnalyticsAnalyticsSettingsPresenterTest {
@Test
fun `present - initial state`() = runTest {
val analyticsPresenter = DefaultAnalyticsPreferencesPresenter(FakeAnalyticsService(), aBuildMeta())
val presenter = AnalyticsSettingsPresenter(
analyticsPresenter,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.analyticsState.isEnabled).isFalse()
}
}
}

View file

@ -22,6 +22,9 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
@ -31,10 +34,12 @@ import org.junit.Test
class DeveloperSettingsPresenterTest {
@Test
fun `present - ensures initial state is correct`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(),
rageshakePresenter
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -43,16 +48,22 @@ class DeveloperSettingsPresenterTest {
assertThat(initialState.features).isEmpty()
assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.cacheSize).isEqualTo(Async.Uninitialized)
val loadedState = awaitItem()
assertThat(loadedState.rageshakeState.isEnabled).isFalse()
assertThat(loadedState.rageshakeState.isSupported).isTrue()
assertThat(loadedState.rageshakeState.sensitivity).isEqualTo(1.0f)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - ensures feature list is loaded`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(),
rageshakePresenter,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -66,10 +77,12 @@ class DeveloperSettingsPresenterTest {
@Test
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(),
rageshakePresenter,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -88,11 +101,13 @@ class DeveloperSettingsPresenterTest {
@Test
fun `present - clear cache`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val clearCacheUseCase = FakeClearCacheUseCase()
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
clearCacheUseCase,
rageshakePresenter,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()

View file

@ -20,42 +20,45 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.impl.preferences.DefaultAnalyticsPreferencesPresenter
import io.element.android.features.analytics.test.A_BUILD_META
import io.element.android.features.analytics.test.FakeAnalyticsService
import io.element.android.features.logout.impl.DefaultLogoutPreferencePresenter
import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import kotlinx.coroutines.test.runTest
import org.junit.Test
class PreferencesRootPresenterTest {
@Test
fun `present - initial state`() = runTest {
val logoutPresenter = DefaultLogoutPreferencePresenter(FakeMatrixClient())
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val analyticsPresenter = DefaultAnalyticsPreferencesPresenter(FakeAnalyticsService(), A_BUILD_META)
val matrixClient = FakeMatrixClient()
val logoutPresenter = DefaultLogoutPreferencePresenter(matrixClient)
val presenter = PreferencesRootPresenter(
logoutPresenter,
rageshakePresenter,
analyticsPresenter,
A_BUILD_META.buildType
matrixClient,
FakeSessionVerificationService(),
BuildType.DEBUG,
FakeVersionFormatter()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.logoutState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.analyticsState.isEnabled).isFalse()
assertThat(initialState.rageshakeState.isEnabled).isTrue()
assertThat(initialState.rageshakeState.isSupported).isTrue()
assertThat(initialState.rageshakeState.sensitivity).isEqualTo(1.0f)
assertThat(initialState.myUser).isEqualTo(Async.Uninitialized)
assertThat(initialState.showDeveloperSettings).isEqualTo(true)
assertThat(initialState.myUser).isNull()
assertThat(initialState.version).isEqualTo("A Version")
val loadedState = awaitItem()
assertThat(loadedState.logoutState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(loadedState.myUser).isEqualTo(
MatrixUser(
userId = matrixClient.sessionId,
displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL
)
)
assertThat(loadedState.showDeveloperSettings).isEqualTo(true)
}
}
}

View file

@ -17,8 +17,6 @@
package io.element.android.features.rageshake.api.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.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -36,7 +34,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun RageshakePreferencesView(
state: RageshakePreferencesState,
modifier: Modifier = Modifier,
onOpenRageshake: () -> Unit = {},
) {
fun onSensitivityChanged(sensitivity: Float) {
state.eventSink(RageshakePreferencesEvents.SetSensitivity(sensitivity = sensitivity))
@ -47,13 +44,6 @@ fun RageshakePreferencesView(
}
Column(modifier = modifier) {
PreferenceCategory(title = stringResource(id = CommonStrings.action_report_bug)) {
PreferenceText(
title = stringResource(id = CommonStrings.action_report_bug),
icon = Icons.Default.BugReport,
onClick = onOpenRageshake
)
}
PreferenceCategory(title = stringResource(id = CommonStrings.settings_rageshake)) {
if (state.isSupported) {
PreferenceSwitch(

View file

@ -23,7 +23,6 @@ sealed interface BugReportEvents {
data class SetDescription(val description: String) : BugReportEvents
data class SetSendLog(val sendLog: Boolean) : BugReportEvents
data class SetSendCrashLog(val sendCrashlog: Boolean) : BugReportEvents
data class SetCanContact(val canContact: Boolean) : BugReportEvents
data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvents
}

View file

@ -16,8 +16,10 @@
package io.element.android.features.rageshake.impl.bugreport
import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@ -26,8 +28,9 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.features.rageshake.impl.bugreport.BugReportPresenter
import io.element.android.libraries.androidutils.system.toast
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.ui.strings.CommonStrings
@ContributesNode(AppScope::class)
class BugReportNode @AssistedInject constructor(
@ -39,10 +42,15 @@ class BugReportNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = LocalContext.current as? Activity
BugReportView(
state = state,
modifier = modifier,
onDone = this::onDone
onBackPressed = { navigateUp() },
onDone = {
activity?.toast(CommonStrings.common_report_submitted)
onDone()
},
)
}

View file

@ -100,9 +100,6 @@ class BugReportPresenter @Inject constructor(
is BugReportEvents.SetCanContact -> updateFormState(formState) {
copy(canContact = event.canContact)
}
is BugReportEvents.SetSendCrashLog -> updateFormState(formState) {
copy(sendCrashLogs = event.sendCrashlog)
}
is BugReportEvents.SetSendLog -> updateFormState(formState) {
copy(sendLogs = event.sendLog)
}
@ -138,7 +135,7 @@ class BugReportPresenter @Inject constructor(
bugReporter.sendBugReport(
reportType = ReportType.BUG_REPORT,
withDevicesLogs = formState.sendLogs,
withCrashLogs = hasCrashLogs && formState.sendCrashLogs,
withCrashLogs = hasCrashLogs && formState.sendLogs,
withKeyRequestHistory = false,
withScreenshot = formState.sendScreenshot,
theBugDescription = formState.description,

View file

@ -36,7 +36,6 @@ data class BugReportState(
data class BugReportFormState(
val description: String,
val sendLogs: Boolean,
val sendCrashLogs: Boolean,
val canContact: Boolean,
val sendScreenshot: Boolean
) : Parcelable {
@ -44,7 +43,6 @@ data class BugReportFormState(
val Default = BugReportFormState(
description = "",
sendLogs = true,
sendCrashLogs = true,
canContact = false,
sendScreenshot = false
)

View file

@ -17,16 +17,11 @@
package io.element.android.features.rageshake.impl.bugreport
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -35,21 +30,20 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import io.element.android.features.rageshake.impl.R
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.LabelledCheckbox
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.components.preferences.PreferenceRow
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.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
@ -63,8 +57,9 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun BugReportView(
state: BugReportState,
onDone: () -> Unit,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
onDone: () -> Unit = { },
) {
LogCompositions(tag = "Rageshake", msg = "Root")
val eventSink = state.eventSink
@ -75,56 +70,27 @@ fun BugReportView(
}
return
}
Box(
modifier = modifier
.fillMaxSize()
.systemBarsPadding()
.imePadding()
) {
Column(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.padding(horizontal = 16.dp),
Box(modifier = modifier) {
PreferenceView(
title = stringResource(id = CommonStrings.common_report_a_bug),
onBackPressed = onBackPressed
) {
val isError = state.sending is Async.Failure
val isFormEnabled = state.sending !is Async.Loading
// Title
Text(
text = stringResource(id = CommonStrings.action_report_bug),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
fontSize = 24.sp,
color = MaterialTheme.colorScheme.primary,
)
// Form
Text(
text = stringResource(id = R.string.screen_bug_report_editor_description),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
fontSize = 16.sp,
color = MaterialTheme.colorScheme.primary,
)
var descriptionFieldState by textFieldState(
stateValue = state.formState.description
)
Column(
// modifier = Modifier.weight(1f),
) {
Spacer(modifier = Modifier.height(16.dp))
PreferenceRow {
OutlinedTextField(
value = descriptionFieldState,
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
modifier = Modifier.fillMaxWidth(),
enabled = isFormEnabled,
label = {
Text(text = stringResource(id = R.string.screen_bug_report_editor_placeholder))
},
supportingText = {
Text(text = stringResource(id = R.string.screen_bug_report_editor_supporting))
Text(text = stringResource(id = R.string.screen_bug_report_editor_description))
},
onValueChange = {
descriptionFieldState = it
@ -134,35 +100,31 @@ fun BugReportView(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next
),
minLines = 3,
// TODO Error text too short
)
}
LabelledCheckbox(
checked = state.formState.sendLogs,
Spacer(modifier = Modifier.height(16.dp))
PreferenceSwitch(
isChecked = state.formState.sendLogs,
onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) },
enabled = isFormEnabled,
text = stringResource(id = R.string.screen_bug_report_include_logs)
title = stringResource(id = R.string.screen_bug_report_include_logs),
subtitle = stringResource(id = R.string.screen_bug_report_logs_description),
)
if (state.hasCrashLogs) {
LabelledCheckbox(
checked = state.formState.sendCrashLogs,
onCheckedChange = { eventSink(BugReportEvents.SetSendCrashLog(it)) },
enabled = isFormEnabled,
text = stringResource(id = R.string.screen_bug_report_include_crash_logs)
)
}
LabelledCheckbox(
checked = state.formState.canContact,
PreferenceSwitch(
isChecked = state.formState.canContact,
onCheckedChange = { eventSink(BugReportEvents.SetCanContact(it)) },
enabled = isFormEnabled,
text = stringResource(id = R.string.screen_bug_report_contact_me)
title = stringResource(id = R.string.screen_bug_report_contact_me_title),
subtitle = stringResource(id = R.string.screen_bug_report_contact_me),
)
if (state.screenshotUri != null) {
LabelledCheckbox(
checked = state.formState.sendScreenshot,
PreferenceSwitch(
isChecked = state.formState.sendScreenshot,
onCheckedChange = { eventSink(BugReportEvents.SetSendScreenshot(it)) },
enabled = isFormEnabled,
text = stringResource(id = R.string.screen_bug_report_include_screenshot)
title = stringResource(id = R.string.screen_bug_report_include_screenshot)
)
if (state.formState.sendScreenshot) {
Box(
@ -183,16 +145,19 @@ fun BugReportView(
}
}
// Submit
Button(
onClick = { eventSink(BugReportEvents.SendBugReport) },
enabled = state.submitEnabled,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 32.dp)
) {
Text(text = stringResource(id = CommonStrings.action_send))
PreferenceRow {
Button(
onClick = { eventSink(BugReportEvents.SendBugReport) },
enabled = state.submitEnabled,
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp, bottom = 16.dp)
) {
Text(text = stringResource(id = CommonStrings.action_send))
}
}
}
when (state.sending) {
is Async.Loading -> {
// Indeterminate indicator, to avoid the freeze effect if the connection takes time to initialize.
@ -219,5 +184,9 @@ fun BugReportViewDarkPreview(@PreviewParameter(BugReportStateProvider::class) st
@Composable
private fun ContentToPreview(state: BugReportState) {
BugReportView(state = state)
BugReportView(
state = state,
onDone = {},
onBackPressed = {},
)
}

View file

@ -38,7 +38,7 @@ class DefaultBugReportEntryPoint @Inject constructor() : BugReportEntryPoint {
}
override fun build(): Node {
return parentNode.createNode<BugReportNode>(buildContext)
return parentNode.createNode<BugReportNode>(buildContext, plugins)
}
}
}

View file

@ -25,7 +25,7 @@ import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.rageshake.api.rageshake.RageshakeDataStore
import io.element.android.libraries.core.bool.orTrue
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import kotlinx.coroutines.flow.Flow
@ -45,7 +45,7 @@ class PreferencesRageshakeDataStore @Inject constructor(
override fun isEnabled(): Flow<Boolean> {
return store.data.map { prefs ->
prefs[enabledKey].orTrue()
prefs[enabledKey].orFalse()
}
}

View file

@ -92,26 +92,6 @@ class BugReportPresenterTest {
}
}
@Test
fun `present - send crash logs`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(),
FakeScreenshotHolder(),
this,
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
// Since this is true by default, start by disabling
initialState.eventSink.invoke(BugReportEvents.SetSendCrashLog(false))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendCrashLogs = false))
initialState.eventSink.invoke(BugReportEvents.SetSendCrashLog(true))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendCrashLogs = true))
}
}
@Test
fun `present - send logs`() = runTest {
val presenter = BugReportPresenter(

View file

@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
const val A_SENSITIVITY = 1f
class FakeRageshakeDataStore(
isEnabled: Boolean = true,
isEnabled: Boolean = false,
sensitivity: Float = A_SENSITIVITY,
) : RageshakeDataStore {

View file

@ -134,7 +134,7 @@ fun RoomDetailsView(
RoomMemberMainActionsSection(onShareUser = ::onShareMember)
}
}
Spacer(Modifier.height(26.dp))
Spacer(Modifier.height(18.dp))
if (state.roomTopic !is RoomTopicState.Hidden) {
TopicSection(

View file

@ -26,10 +26,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
import io.element.android.libraries.architecture.Presenter
@ -43,9 +43,9 @@ import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsS
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomSummary
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.user.getCurrentUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import kotlinx.collections.immutable.ImmutableList
@ -162,13 +162,7 @@ class RoomListPresenter @Inject constructor(
}
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
val userDisplayName = client.loadUserDisplayName().getOrNull()
matrixUser.value = MatrixUser(
userId = UserId(client.sessionId.value),
displayName = userDisplayName,
avatarUrl = userAvatarUrl,
)
matrixUser.value = client.getCurrentUser()
}
private fun updateVisibleRange(range: IntRange) {

View file

@ -50,8 +50,9 @@ class RoomListPresenterTests {
@Test
fun `present - should start with no user and then load user with success`() = runTest {
val matrixClient = FakeMatrixClient()
val presenter = RoomListPresenter(
FakeMatrixClient(),
matrixClient,
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@ -75,11 +76,12 @@ class RoomListPresenterTests {
@Test
fun `present - should start with no user and then load user with error`() = runTest {
val matrixClient = FakeMatrixClient(
userDisplayName = Result.failure(AN_EXCEPTION),
userAvatarURLString = Result.failure(AN_EXCEPTION),
)
val presenter = RoomListPresenter(
FakeMatrixClient(
userDisplayName = Result.failure(AN_EXCEPTION),
userAvatarURLString = Result.failure(AN_EXCEPTION),
),
matrixClient,
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@ -100,8 +102,9 @@ class RoomListPresenterTests {
@Test
fun `present - should filter room with success`() = runTest {
val matrixClient = FakeMatrixClient()
val presenter = RoomListPresenter(
FakeMatrixClient(),
matrixClient,
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@ -127,10 +130,11 @@ class RoomListPresenterTests {
@Test
fun `present - load 1 room with success`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
)
val presenter = RoomListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
),
matrixClient,
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@ -159,10 +163,11 @@ class RoomListPresenterTests {
@Test
fun `present - load 1 room with success and filter rooms`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
)
val presenter = RoomListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
),
matrixClient,
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@ -197,10 +202,11 @@ class RoomListPresenterTests {
@Test
fun `present - update visible range`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
)
val presenter = RoomListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
),
matrixClient,
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@ -245,10 +251,11 @@ class RoomListPresenterTests {
@Test
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
)
val presenter = RoomListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
),
matrixClient,
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService().apply {
@ -274,8 +281,9 @@ class RoomListPresenterTests {
@Test
fun `present - sets invite state`() = runTest {
val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites)
val matrixClient = FakeMatrixClient()
val presenter = RoomListPresenter(
FakeMatrixClient(),
matrixClient,
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@ -304,8 +312,9 @@ class RoomListPresenterTests {
@Test
fun `present - show context menu`() = runTest {
val matrixClient = FakeMatrixClient()
val presenter = RoomListPresenter(
FakeMatrixClient(),
matrixClient,
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@ -331,8 +340,9 @@ class RoomListPresenterTests {
@Test
fun `present - hide context menu`() = runTest {
val matrixClient = FakeMatrixClient()
val presenter = RoomListPresenter(
FakeMatrixClient(),
matrixClient,
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
@ -363,8 +373,9 @@ class RoomListPresenterTests {
@Test
fun `present - leave room calls into leave room presenter`() = runTest {
val leaveRoomPresenter = LeaveRoomPresenterFake()
val matrixClient = FakeMatrixClient()
val presenter = RoomListPresenter(
FakeMatrixClient(),
matrixClient,
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),

View file

@ -161,7 +161,7 @@ maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.0"
# Analytics
posthog = "com.posthog.android:posthog:2.0.3"
sentry_android = "io.sentry:sentry-android:6.24.0"
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:main-SNAPSHOT"
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:42b2faa417c1e95f430bf8f6e379adba25ad5ef8"
# Di
inject = "javax.inject:javax.inject:1"
@ -193,7 +193,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
anvil = { id = "com.squareup.anvil", version.ref = "anvil" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
ktlint = "org.jlleitschuh.gradle.ktlint:11.4.2"
ktlint = "org.jlleitschuh.gradle.ktlint:11.5.0"
dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" }
dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" }
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" }

View file

@ -23,6 +23,7 @@ data class BuildMeta(
val applicationId: String,
val lowPrivacyLoggingEnabled: Boolean,
val versionName: String,
val versionCode: Int,
val gitRevision: String,
val gitRevisionDate: String,
val gitBranchName: String,

View file

@ -50,6 +50,10 @@ fun OnBoardingPage(
footer: @Composable () -> Unit = {},
content: @Composable () -> Unit = {},
) {
// Note: having a night variant of R.drawable.onboarding_bg in the folder `drawable-night` is working
// at runtime, but is not in Android Studio Preview. So I prefer to handle this manually.
val isLight = ElementTheme.colors.isLight
val bgDrawableRes = if (isLight) R.drawable.onboarding_bg_light else R.drawable.onboarding_bg_dark
Box(
modifier = modifier
.fillMaxSize()
@ -58,7 +62,7 @@ fun OnBoardingPage(
Image(
modifier = Modifier
.fillMaxSize(),
painter = painterResource(id = R.drawable.onboarding_bg),
painter = painterResource(id = bgDrawableRes),
contentScale = ContentScale.Crop,
contentDescription = null,
)

View file

@ -27,6 +27,8 @@ enum class AvatarSize(val dp: Dp) {
ForwardRoomListItem(36.dp),
UserPreference(56.dp),
UserHeader(96.dp),
UserListItem(36.dp),

View file

@ -18,7 +18,6 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.ui.unit.dp
internal val preferenceMinHeightOnlyTitle = 48.dp
internal val preferenceMinHeight = 64.dp
internal val preferenceMinHeightOnlyTitle = 56.dp
internal val preferenceMinHeight = 56.dp
internal val preferencePaddingHorizontal = 16.dp
internal val preferencePaddingVertical = 16.dp

View file

@ -23,15 +23,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Announcement
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.theme.ElementTheme
@Composable
fun PreferenceCategory(
@ -49,7 +48,7 @@ fun PreferenceCategory(
}
content()
if (showDivider) {
Divider()
PreferenceDivider()
}
}
}
@ -57,9 +56,14 @@ fun PreferenceCategory(
@Composable
fun PreferenceCategoryTitle(title: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = modifier.padding(
top = 20.dp,
bottom = 8.dp,
start = preferencePaddingHorizontal,
end = preferencePaddingHorizontal,
),
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.materialColors.primary,
text = title,
)
}
@ -85,7 +89,8 @@ private fun ContentToPreview() {
PreferenceSlide(
title = "Slide",
summary = "Summary",
value = 0.75F
value = 0.75F,
showIconAreaIfNoIcon = true,
)
}
}

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Announcement
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 androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Checkbox
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.theme.ElementTheme
@Composable
fun PreferenceCheckbox(
title: String,
isChecked: Boolean,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: ImageVector? = null,
showIconAreaIfNoIcon: Boolean = false,
onCheckedChange: (Boolean) -> Unit = {},
) {
Row(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight)
.clickable { onCheckedChange(!isChecked) }
.padding(vertical = 4.dp, horizontal = preferencePaddingHorizontal),
verticalAlignment = Alignment.CenterVertically
) {
PreferenceIcon(
icon = icon,
enabled = enabled,
isVisible = showIconAreaIfNoIcon
)
Text(
modifier = Modifier
.weight(1f),
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = enabled.toEnabledColor(),
)
Checkbox(
modifier = Modifier
.align(Alignment.CenterVertically),
checked = isChecked,
enabled = enabled,
onCheckedChange = onCheckedChange
)
}
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceCheckboxPreview() = ElementThemedPreview { ContentToPreview() }
@Composable
private fun ContentToPreview() {
PreferenceCheckbox(
title = "Checkbox",
icon = Icons.Default.Announcement,
enabled = true,
isChecked = true
)
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.theme.ElementTheme
@Composable
fun PreferenceDivider(
modifier: Modifier = Modifier,
) {
Divider(
modifier = modifier,
color = ElementTheme.colors.borderDisabled,
)
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceDividerPreview() = ElementThemedPreview { ContentToPreview() }
@Composable
private fun ContentToPreview() {
PreferenceDivider()
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Text
/**
* Simple Row with which follow design for preferences.
*/
@Composable
fun PreferenceRow(
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit,
) {
Row(
modifier = modifier
.padding(horizontal = preferencePaddingHorizontal)
.heightIn(min = preferenceMinHeight)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
content()
}
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceRowPreview() = ElementThemedPreview { ContentToPreview() }
@Composable
private fun ContentToPreview() {
PreferenceRow {
Text(text = "Content")
}
}

View file

@ -19,7 +19,6 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
@ -34,19 +33,16 @@ import androidx.compose.material.icons.filled.Announcement
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Divider
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.theme.ElementTheme
@OptIn(ExperimentalLayoutApi::class)
@Composable
@ -94,15 +90,12 @@ fun PreferenceTopAppBar(
BackButton(onClick = onBackPressed)
},
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Text(
text = title,
style = ElementTheme.typography.fontHeadingSmMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
@ -129,17 +122,24 @@ private fun ContentToPreview() {
subtitle = "Some other text",
icon = Icons.Default.BugReport,
)
Divider()
PreferenceDivider()
PreferenceSwitch(
title = "Switch",
icon = Icons.Default.Announcement,
isChecked = true,
)
Divider()
PreferenceDivider()
PreferenceCheckbox(
title = "Checkbox",
icon = Icons.Default.Announcement,
isChecked = true,
)
PreferenceDivider()
PreferenceSlide(
title = "Slide",
summary = "Summary",
value = 0.75F
value = 0.75F,
showIconAreaIfNoIcon = true,
)
}
}

View file

@ -17,7 +17,6 @@
package io.element.android.libraries.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
@ -25,17 +24,18 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Slider
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.theme.ElementTheme
@Composable
fun PreferenceSlide(
@ -44,45 +44,41 @@ fun PreferenceSlide(
value: Float,
modifier: Modifier = Modifier,
icon: ImageVector? = null,
showIconAreaIfNoIcon: Boolean = false,
enabled: Boolean = true,
summary: String? = null,
steps: Int = 0,
onValueChange: (Float) -> Unit = {},
) {
Box(
Row(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight)
.padding(top = preferencePaddingVertical),
.padding(vertical = 4.dp, horizontal = preferencePaddingHorizontal),
) {
Row(modifier = Modifier.fillMaxWidth()) {
PreferenceIcon(icon = icon)
Column(
modifier = Modifier
.weight(1f)
.padding(end = preferencePaddingHorizontal),
) {
PreferenceIcon(icon = icon, isVisible = showIconAreaIfNoIcon)
Column(
modifier = Modifier
.weight(1f),
) {
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = enabled.toEnabledColor(),
)
summary?.let {
Text(
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyLarge,
style = ElementTheme.typography.fontBodyMdRegular,
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,
enabled = enabled,
)
}
}
}

View file

@ -17,65 +17,84 @@
package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Announcement
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
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 androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Checkbox
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
import io.element.android.libraries.theme.ElementTheme
@Composable
fun PreferenceSwitch(
title: String,
isChecked: Boolean,
modifier: Modifier = Modifier,
subtitle: String? = null,
enabled: Boolean = true,
icon: ImageVector? = null,
showIconAreaIfNoIcon: Boolean = false,
onCheckedChange: (Boolean) -> Unit = {},
switchAlignment: Alignment.Vertical = Alignment.CenterVertically
) {
Box(
Row(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight)
.clickable { onCheckedChange(!isChecked) },
contentAlignment = Alignment.CenterStart
.clickable { onCheckedChange(!isChecked) }
.padding(vertical = 4.dp, horizontal = preferencePaddingHorizontal),
verticalAlignment = Alignment.CenterVertically
) {
Row(modifier = Modifier.fillMaxWidth()) {
PreferenceIcon(
modifier = Modifier.padding(vertical = preferencePaddingVertical),
icon = icon,
enabled = enabled
)
PreferenceIcon(
icon = icon,
enabled = enabled,
isVisible = showIconAreaIfNoIcon
)
Column(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
) {
Text(
modifier = Modifier
.weight(1f)
.padding(vertical = preferencePaddingVertical),
style = MaterialTheme.typography.bodyLarge,
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = enabled.toEnabledColor(),
text = title
)
Checkbox(
modifier = Modifier
.padding(end = preferencePaddingHorizontal)
.align(Alignment.CenterVertically),
checked = isChecked,
enabled = enabled,
onCheckedChange = onCheckedChange
)
if (subtitle != null) {
Spacer(modifier = Modifier.height(4.dp))
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = subtitle,
color = enabled.toSecondaryEnabledColor(),
)
}
}
Spacer(modifier = Modifier.width(16.dp))
// TODO Create a wrapper for Switch
Switch(
modifier = Modifier
.align(switchAlignment),
checked = isChecked,
enabled = enabled,
onCheckedChange = onCheckedChange
)
}
}
@ -87,6 +106,7 @@ internal fun PreferenceSwitchPreview() = ElementThemedPreview { ContentToPreview
private fun ContentToPreview() {
PreferenceSwitch(
title = "Switch",
subtitle = "Subtitle Switch",
icon = Icons.Default.Announcement,
enabled = true,
isChecked = true

View file

@ -18,19 +18,15 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -43,75 +39,75 @@ import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.theme.ElementTheme
/**
* Tried to use ListItem, but it cannot really match the design. Keep custom Layout for now.
*/
@Composable
fun PreferenceText(
title: String?,
title: String,
modifier: Modifier = Modifier,
subtitle: String? = null,
currentValue: String? = null,
loadingCurrentValue: Boolean = false,
icon: ImageVector? = null,
showIconAreaIfNoIcon: Boolean = false,
tintColor: Color? = null,
onClick: () -> Unit = {},
) {
val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight
Box(
Row(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = minHeight)
.clickable { onClick() }
.padding(end = preferencePaddingHorizontal),
.padding(horizontal = preferencePaddingHorizontal, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Row(
PreferenceIcon(
icon = icon,
isVisible = showIconAreaIfNoIcon,
tintColor = tintColor ?: ElementTheme.materialColors.secondary
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = preferencePaddingVertical)
.weight(1f)
.align(Alignment.CenterVertically)
) {
PreferenceIcon(icon = icon, tintColor = tintColor)
Column(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
) {
if (title != null) {
Text(
style = MaterialTheme.typography.bodyLarge,
text = title,
color = tintColor ?: MaterialTheme.colorScheme.primary,
)
}
if (title != null && subtitle != null) {
Spacer(modifier = Modifier.height(8.dp))
}
if (subtitle != null) {
Text(
style = MaterialTheme.typography.bodyMedium,
text = subtitle,
color = tintColor ?: MaterialTheme.colorScheme.tertiary,
)
}
}
if (currentValue != null) {
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = tintColor ?: ElementTheme.materialColors.primary,
)
if (subtitle != null) {
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(horizontal = 16.dp),
text = currentValue,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary,
)
} else if (loadingCurrentValue) {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.padding(horizontal = 16.dp)
.size(20.dp)
.align(Alignment.CenterVertically),
strokeWidth = 2.dp
style = ElementTheme.typography.fontBodyMdRegular,
text = subtitle,
color = tintColor ?: ElementTheme.materialColors.secondary,
)
}
}
if (currentValue != null) {
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 16.dp, end = 8.dp),
text = currentValue,
style = ElementTheme.typography.fontBodyXsMedium,
color = ElementTheme.materialColors.secondary,
)
} else if (loadingCurrentValue) {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.padding(start = 16.dp, end = 8.dp)
.size(20.dp)
.align(Alignment.CenterVertically),
strokeWidth = 2.dp
)
}
}
}
@ -155,5 +151,14 @@ private fun ContentToPreview() {
icon = Icons.Default.BugReport,
loadingCurrentValue = true,
)
PreferenceText(
title = "Title no icon with icon area",
showIconAreaIfNoIcon = true,
loadingCurrentValue = true,
)
PreferenceText(
title = "Title no icon",
loadingCurrentValue = true,
)
}
}

View file

@ -17,8 +17,8 @@
package io.element.android.libraries.designsystem.components.preferences.components
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -46,14 +46,11 @@ fun PreferenceIcon(
contentDescription = "",
tint = tintColor ?: enabled.toSecondaryEnabledColor(),
modifier = modifier
.padding(start = 8.dp)
.width(48.dp)
.heightIn(max = 48.dp),
.padding(end = 16.dp)
.size(24.dp),
)
} else if (isVisible) {
Spacer(modifier = modifier.width(56.dp))
} else {
Spacer(modifier = modifier.width(16.dp))
Spacer(modifier = modifier.width(40.dp))
}
}

View file

@ -68,6 +68,7 @@ fun OutlinedTextField(
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = OutlinedTextFieldDefaults.shape,
colors: TextFieldColors = OutlinedTextFieldDefaults.colors()
@ -90,6 +91,7 @@ fun OutlinedTextField(
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
interactionSource = interactionSource,
shape = shape,
colors = colors,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

View file

@ -20,7 +20,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceCheckbox
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
@ -52,8 +52,7 @@ fun FeaturePreferenceView(
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
PreferenceSwitch(
PreferenceCheckbox(
title = feature.title,
isChecked = feature.isEnabled,
modifier = modifier,

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.user
import io.element.android.libraries.matrix.api.MatrixClient
/**
* Get the current user, as [MatrixUser], using [MatrixClient.loadUserAvatarURLString]
* and [MatrixClient.loadUserDisplayName].
*/
suspend fun MatrixClient.getCurrentUser(): MatrixUser {
val userAvatarUrl = loadUserAvatarURLString().getOrNull()
val userDisplayName = loadUserDisplayName().getOrNull()
return MatrixUser(
userId = sessionId,
displayName = userDisplayName,
avatarUrl = userAvatarUrl,
)
}

View file

@ -36,7 +36,6 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.media.sendAttachment
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.timelineDiffFlow
import io.element.android.services.toolbox.api.systemclock.SystemClock

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.test.core
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
fun aBuildMeta(
buildType: BuildType = BuildType.DEBUG,
isDebuggable: Boolean = true,
applicationName: String = "",
applicationId: String = "",
lowPrivacyLoggingEnabled: Boolean = true,
versionName: String = "",
versionCode: Int = 0,
gitRevision: String = "",
gitRevisionDate: String = "",
gitBranchName: String = "",
flavorDescription: String = "",
flavorShortDescription: String = "",
) = BuildMeta(
buildType,
isDebuggable,
applicationName,
applicationId,
lowPrivacyLoggingEnabled,
versionName,
versionCode,
gitRevision,
gitRevisionDate,
gitBranchName,
flavorDescription,
flavorShortDescription
)

View file

@ -16,23 +16,19 @@
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
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.material3.MaterialTheme
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -41,44 +37,66 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.theme.ElementTheme
@Composable
fun MatrixUserHeader(
matrixUser: MatrixUser?,
modifier: Modifier = Modifier,
// TODO handle click on this item, to let the user be able to update their profile.
// onClick: () -> Unit = {},
) {
if (matrixUser == null) {
MatrixUserHeaderPlaceholder(modifier = modifier)
} else {
MatrixUserHeaderContent(
matrixUser = matrixUser,
modifier = modifier,
// onClick = onClick
)
}
}
@Composable
private fun MatrixUserHeaderContent(
matrixUser: MatrixUser,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
// onClick: () -> Unit = {},
) {
Column(
Row(
modifier = modifier
.clickable(onClick = onClick)
// .clickable(onClick = onClick)
.fillMaxWidth()
.padding(all = 16.dp)
.height(IntrinsicSize.Min),
horizontalAlignment = Alignment.CenterHorizontally
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Avatar(
matrixUser.getAvatarData(size = AvatarSize.UserHeader),
modifier = Modifier
.padding(vertical = 12.dp),
avatarData = matrixUser.getAvatarData(size = AvatarSize.UserPreference),
)
Spacer(modifier = Modifier.height(16.dp))
// Name
Text(
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
text = matrixUser.getBestName(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.primary,
)
// Id
if (matrixUser.displayName.isNullOrEmpty().not()) {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
// Name
Text(
text = matrixUser.userId.value,
color = MaterialTheme.colorScheme.secondary,
fontSize = 14.sp,
text = matrixUser.getBestName(),
maxLines = 1,
overflow = TextOverflow.Ellipsis
style = ElementTheme.typography.fontHeadingSmMedium,
overflow = TextOverflow.Ellipsis,
color = ElementTheme.materialColors.primary,
)
// Id
if (matrixUser.displayName.isNullOrEmpty().not()) {
Text(
text = matrixUser.userId.value,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.materialColors.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.roomListPlaceholder
import io.element.android.libraries.theme.ElementTheme
@Composable
fun MatrixUserHeaderPlaceholder(
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.padding(vertical = 12.dp)
.size(AvatarSize.UserPreference.dp)
.background(color = ElementTheme.colors.roomListPlaceholder, shape = CircleShape)
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
PlaceholderAtom(width = 80.dp, height = 7.dp)
Spacer(modifier = Modifier.height(16.dp))
PlaceholderAtom(width = 180.dp, height = 6.dp)
}
}
}
@Preview
@Composable
fun MatrixUserHeaderPlaceholderLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
fun MatrixUserHeaderPlaceholderDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
MatrixUserHeaderPlaceholder()
}

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e3ccec7a26ef11d01dfed40d56937c9bf9c1411680b2ff7338cf10f9541a3880
size 23871
oid sha256:940c3ac11da74a6eb734085c468050040c2d31056b6d8de516e49ddb0058c9ee
size 23412

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00a807f1ac722935a0dad2e82d56ba123cad3e92d2f74a465b2c92497c18f4f7
size 25372
oid sha256:97b9796def982afb2d5611db431489a2a42f80f11f949ea4aaab41fd01f87cb1
size 23478

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8e5daf50829c990b33d22b20f2c4f75b526f6800be2d3966c5ca321ebc3a45c2
size 8830
oid sha256:e6fe0e5d16dc3e2fcca6f892366b2742d72f32fba4b5973791a139d812d44f95
size 7010

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cd59f2b1389ab9d0d9faa993fcd150e09291920563341f452c8f48d4fe27089e
size 8678
oid sha256:cf330565d0d920c45815781c9619dd063822a53746f28087b6ae8fef729d4dd6
size 6958

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:93babf0a42a224e2d2288cf57141e2e766aef29725f6cc2c2c54d91fe1da70fa
size 251481
oid sha256:1c001e4db1cee87d99b4faa8956d322dd5f70c1a3fe92ea36ccd24ba7f6ee94b
size 471797

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:19243cd6c9bccd091b807b808b94d77aebc61696aded13bdeea60eabaaf70513
size 241183
oid sha256:188c11dc1c5eca937a0e8aa20b71ea6ec0436a7d4139cb2294feb25f72037ab3
size 454206

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:79cfac58781ea6924533968f7b7912afc18d307b3df4a607e5aa91da7f7e5336
size 250927
oid sha256:887b8771ed039e5e4b8da51a567e73a886606497d0dd4906a89513ea5e3ce3e7
size 470644

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e8fd4cb80a1fd482faf0de7e70ec6788fcf77904df36424dd502afe2817d49a2
size 237499
oid sha256:7f7742fadef66ce01fffab3b8ba7ce2b7ac81977300b9bf541f7d10d3dd7650b
size 442213

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a6e438c1c7949d8f6dd1dd98036cfebc72f79d77a846aa470e19656e3e10217
size 242094
oid sha256:c809d004a829c2bc6b900e139c0c1e7379249403741646ad92bc1c2c9f248903
size 354381

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6f4d0f7fbbcb2bad92d973678669d3dd6a99c8d3c149b4e138beafcdd47dae19
size 231833
oid sha256:15c75a82f4cd2e26108898622eaba99a2720fe9733d12c418b2eaacde009d9bb
size 344302

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3e34ebd0eddf70241af88ecbb3ffe8fee6eaa438843bda056acb3f5a105ade98
size 241836
oid sha256:b9e4204700497f29a073ff238f5d42b1373e346a995e242861ec2ba1ebd6f606
size 356483

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2f4fa21fb8352153b69e80003fd0fa014c7cbb8db42259e0edb6584794c5bc98
size 227974
oid sha256:726a2c5fcf7d7e6b24799ad62bcd58872730493be0fd8282aab7884e2832d7ac
size 336263

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:63f37eb39f73c5c4e371dc3c2f6b8c1ec5e58acaee666af235b8b15a79c749fa
size 15970

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3dd1385bea0ebe1481c52a16cf32f30e1a9a6812cf3618e472dd321078164830
size 17314

Some files were not shown because too many files have changed in this diff Show more