Merge pull request #742 from vector-im/feature/bma/settingsUi

Settings UI
This commit is contained in:
Benoit Marty 2023-07-04 10:13:14 +02:00 committed by GitHub
commit e7331e8be0
182 changed files with 1736 additions and 715 deletions

2
.idea/kotlinc.xml generated
View file

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

View file

@ -2,12 +2,29 @@ appId: ${APP_ID}
--- ---
- tapOn: - tapOn:
id: "home_screen-settings" id: "home_screen-settings"
- assertVisible: "Rageshake to report bug" - assertVisible: "Settings"
- takeScreenshot: build/maestro/600-Settings - takeScreenshot: build/maestro/600-Settings
- tapOn: - tapOn:
text: "Report bug" text: "Analytics"
index: 1 - assertVisible: "Share analytics data"
- assertVisible: "Describe the bug…"
- back - 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 - back
- runFlow: ../assertions/assertHomeDisplayed.yaml - runFlow: ../assertions/assertHomeDisplayed.yaml

View file

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

View file

@ -31,7 +31,7 @@ import io.element.android.x.R
fun IconPreview( fun IconPreview(
modifier: Modifier = Modifier, 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_background), contentDescription = null)
Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), 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 io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -292,6 +291,10 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onOpenBugReport() { override fun onOpenBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() } plugins<Callback>().forEach { it.onOpenBugReport() }
} }
override fun onVerifyClicked() {
backstack.push(NavTarget.VerifySession)
}
} }
preferencesEntryPoint.nodeBuilder(this, buildContext) preferencesEntryPoint.nodeBuilder(this, buildContext)
.callback(callback) .callback(callback)

View file

@ -225,8 +225,9 @@ koverMerged {
includes += "*Presenter" includes += "*Presenter"
excludes += "*Fake*Presenter" excludes += "*Fake*Presenter"
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" 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.onboarding.impl.OnBoardingPresenter"
excludes += "io.element.android.features.preferences.impl.about.AboutPresenter"
} }
bound { bound {
minValue = 90 minValue = 90

View file

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

View file

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

View file

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

View file

@ -21,8 +21,8 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.api.AnalyticsOptInEvents 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.features.analytics.test.FakeAnalyticsService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -31,7 +31,7 @@ class AnalyticsPreferencesPresenterTest {
fun `present - initial state available`() = runTest { fun `present - initial state available`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter( val presenter = DefaultAnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true), FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
A_BUILD_META aBuildMeta()
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
@ -46,7 +46,7 @@ class AnalyticsPreferencesPresenterTest {
fun `present - initial state not available`() = runTest { fun `present - initial state not available`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter( val presenter = DefaultAnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = false, didAskUserConsent = false), FakeAnalyticsService(isEnabled = false, didAskUserConsent = false),
A_BUILD_META aBuildMeta()
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
@ -60,7 +60,7 @@ class AnalyticsPreferencesPresenterTest {
fun `present - enable and disable`() = runTest { fun `present - enable and disable`() = runTest {
val presenter = DefaultAnalyticsPreferencesPresenter( val presenter = DefaultAnalyticsPreferencesPresenter(
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true), FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
A_BUILD_META aBuildMeta()
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() 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.api.user.MatrixUser
import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient 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.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.usersearch.test.FakeUserRepository import io.element.android.libraries.usersearch.test.FakeUserRepository
import kotlinx.collections.immutable.persistentListOf 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.architecture.Async
import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog 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.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun LogoutPreferenceView( fun LogoutPreferenceView(
@ -81,13 +79,11 @@ fun LogoutPreferenceView(
fun LogoutPreferenceContent( fun LogoutPreferenceContent(
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
PreferenceCategory(title = stringResource(id = CommonStrings.settings_title_general)) { PreferenceText(
PreferenceText( title = stringResource(id = R.string.screen_signout_preference_item),
title = stringResource(id = R.string.screen_signout_preference_item), icon = Icons.Filled.Logout,
icon = Icons.Default.Logout, onClick = onClick
onClick = onClick )
)
}
} }
@Preview @Preview

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_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_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.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.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider
@ -566,19 +567,7 @@ class MessagesPresenterTest {
timelineItemsFactory = aTimelineItemsFactory(), timelineItemsFactory = aTimelineItemsFactory(),
room = matrixRoom, room = matrixRoom,
) )
val buildMeta = BuildMeta( val buildMeta = aBuildMeta()
buildType = BuildType.DEBUG,
isDebuggable = true,
applicationId = "",
applicationName = "",
lowPrivacyLoggingEnabled = true,
versionName = "",
gitRevision = "",
gitBranchName = "",
gitRevisionDate = "",
flavorDescription = "",
flavorShortDescription = "",
)
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta) val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
val customReactionPresenter = CustomReactionPresenter() val customReactionPresenter = CustomReactionPresenter()
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom) 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.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.ActionListState 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.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.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent 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.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.A_MESSAGE
import io.element.android.libraries.matrix.test.core.aBuildMeta
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -74,7 +72,6 @@ class ActionListPresenterTest {
} }
} }
@Test @Test
fun `present - compute for message from others redacted`() = runTest { fun `present - compute for message from others redacted`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true) 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)) private fun anActionListPresenter(isBuildDebuggable: Boolean) = ActionListPresenter(buildMeta = aBuildMeta(isDebuggable = isBuildDebuggable))

View file

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

View file

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

View file

@ -30,6 +30,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.preferences.api.PreferencesEntryPoint 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.developer.DeveloperSettingsNode
import io.element.android.features.preferences.impl.root.PreferencesRootNode import io.element.android.features.preferences.impl.root.PreferencesRootNode
import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.BackstackNode
@ -57,6 +59,12 @@ class PreferencesFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
object DeveloperSettings : NavTarget object DeveloperSettings : NavTarget
@Parcelize
object AnalyticsSettings : NavTarget
@Parcelize
object About : NavTarget
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -67,6 +75,18 @@ class PreferencesFlowNode @AssistedInject constructor(
plugins<PreferencesEntryPoint.Callback>().forEach { it.onOpenBugReport() } 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() { override fun onOpenDeveloperSettings() {
backstack.push(NavTarget.DeveloperSettings) backstack.push(NavTarget.DeveloperSettings)
} }
@ -76,6 +96,12 @@ class PreferencesFlowNode @AssistedInject constructor(
NavTarget.DeveloperSettings -> { NavTarget.DeveloperSettings -> {
createNode<DeveloperSettingsNode>(buildContext) 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. * 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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.core.meta.BuildType
val A_BUILD_META = BuildMeta( open class AboutStateProvider : PreviewParameterProvider<AboutState> {
isDebuggable = true, override val values: Sequence<AboutState>
buildType = BuildType.DEBUG, get() = sequenceOf(
applicationName = "Element X test", aAboutState(),
applicationId = "", )
lowPrivacyLoggingEnabled = false, }
versionName = "",
gitRevision = "", fun aAboutState() = AboutState(
gitRevisionDate = "", elementLegals = getAllLegals(),
gitBranchName = "",
flavorDescription = "",
flavorShortDescription = "",
) )

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,33 +17,61 @@
package io.element.android.features.preferences.impl.root package io.element.android.features.preferences.impl.root
import androidx.compose.runtime.Composable 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.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.architecture.Presenter
import io.element.android.libraries.core.meta.BuildType 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 import javax.inject.Inject
class PreferencesRootPresenter @Inject constructor( class PreferencesRootPresenter @Inject constructor(
private val logoutPresenter: LogoutPreferencePresenter, private val logoutPresenter: LogoutPreferencePresenter,
private val rageshakePresenter: RageshakePreferencesPresenter, private val matrixClient: MatrixClient,
private val analyticsPresenter: AnalyticsPreferencesPresenter, private val sessionVerificationService: SessionVerificationService,
private val buildType: BuildType, private val buildType: BuildType,
private val versionFormatter: VersionFormatter,
) : Presenter<PreferencesRootState> { ) : Presenter<PreferencesRootState> {
@Composable @Composable
override fun present(): PreferencesRootState { 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 logoutState = logoutPresenter.present()
val rageshakeState = rageshakePresenter.present()
val analyticsState = analyticsPresenter.present()
val showDeveloperSettings = buildType != BuildType.RELEASE val showDeveloperSettings = buildType != BuildType.RELEASE
return PreferencesRootState( return PreferencesRootState(
logoutState = logoutState, logoutState = logoutState,
rageshakeState = rageshakeState, myUser = matrixUser.value,
analyticsState = analyticsState, version = versionFormatter.get(),
myUser = Async.Uninitialized, showCompleteVerification = sessionIsNotVerified,
showDeveloperSettings = showDeveloperSettings 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 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.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 import io.element.android.libraries.matrix.api.user.MatrixUser
data class PreferencesRootState( data class PreferencesRootState(
val logoutState: LogoutPreferenceState, val logoutState: LogoutPreferenceState,
val rageshakeState: RageshakePreferencesState, val myUser: MatrixUser?,
val analyticsState: AnalyticsPreferencesState, val version: String,
val myUser: Async<MatrixUser>, val showCompleteVerification: Boolean,
val showDeveloperSettings: Boolean val showDeveloperSettings: Boolean
) )

View file

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

View file

@ -16,36 +16,45 @@
package io.element.android.features.preferences.impl.root 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.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.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter 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.logout.api.LogoutPreferenceView
import io.element.android.features.preferences.impl.user.UserPreferences 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.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceView import io.element.android.libraries.designsystem.components.preferences.PreferenceView
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.LargeHeightPreview 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.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserProvider import io.element.android.libraries.matrix.ui.components.MatrixUserProvider
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun PreferencesRootView( fun PreferencesRootView(
state: PreferencesRootState, state: PreferencesRootState,
onBackPressed: () -> Unit,
onVerifyClicked: () -> Unit,
onOpenAnalytics: () -> Unit,
onOpenRageShake: () -> Unit,
onOpenAbout: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
onOpenRageShake: () -> Unit = {},
onOpenDeveloperSettings: () -> Unit = {},
) { ) {
// TODO Hierarchy!
// Include pref from other modules // Include pref from other modules
PreferenceView( PreferenceView(
modifier = modifier, modifier = modifier,
@ -53,31 +62,55 @@ fun PreferencesRootView(
title = stringResource(id = CommonStrings.common_settings) title = stringResource(id = CommonStrings.common_settings)
) { ) {
UserPreferences(state.myUser) UserPreferences(state.myUser)
AnalyticsPreferencesView( if (state.showCompleteVerification) {
state = state.analyticsState, 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( PreferenceText(
state = state.rageshakeState, title = stringResource(id = CommonStrings.action_report_bug),
onOpenRageshake = onOpenRageShake, icon = Icons.Outlined.BugReport,
onClick = onOpenRageShake
) )
LogoutPreferenceView( PreferenceText(
state = state.logoutState, title = stringResource(id = CommonStrings.common_about),
icon = Icons.Outlined.Help,
onClick = onOpenAbout,
) )
if (state.showDeveloperSettings) { if (state.showDeveloperSettings) {
DeveloperPreferencesView(onOpenDeveloperSettings) 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 @Composable
fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) { fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
PreferenceCategory(title = stringResource(id = CommonStrings.common_developer_options)) { PreferenceText(
PreferenceText( title = stringResource(id = CommonStrings.common_developer_options),
title = stringResource(id = CommonStrings.common_developer_options), icon = Icons.Outlined.DeveloperMode,
icon = Icons.Default.DeveloperMode, onClick = onOpenDeveloperSettings
onClick = onOpenDeveloperSettings )
)
}
} }
@LargeHeightPreview @LargeHeightPreview
@ -92,5 +125,13 @@ fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class)
@Composable @Composable
private fun ContentToPreview(matrixUser: MatrixUser) { 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 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.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter 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.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
@ -32,16 +28,13 @@ import io.element.android.libraries.matrix.ui.components.MatrixUserWithNullProvi
@Composable @Composable
fun UserPreferences( fun UserPreferences(
user: Async<MatrixUser>, user: MatrixUser?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (val userData = user.dataOrNull()) { MatrixUserHeader(
null -> Spacer(modifier = modifier.height(1.dp)) modifier = modifier,
else -> MatrixUserHeader( matrixUser = user
modifier = modifier, )
matrixUser = userData
)
}
} }
@Preview @Preview
@ -56,9 +49,5 @@ internal fun UserPreferencesDarkPreview(@PreviewParameter(MatrixUserWithNullProv
@Composable @Composable
private fun ContentToPreview(matrixUser: MatrixUser?) { private fun ContentToPreview(matrixUser: MatrixUser?) {
if (matrixUser == null) { UserPreferences(matrixUser)
UserPreferences(Async.Uninitialized)
} else {
UserPreferences(Async.Success(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 com.google.common.truth.Truth.assertThat
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase 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.architecture.Async
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
@ -31,10 +34,12 @@ import org.junit.Test
class DeveloperSettingsPresenterTest { class DeveloperSettingsPresenterTest {
@Test @Test
fun `present - ensures initial state is correct`() = runTest { fun `present - ensures initial state is correct`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val presenter = DeveloperSettingsPresenter( val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(), FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(), FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(), FakeClearCacheUseCase(),
rageshakePresenter
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
@ -43,16 +48,22 @@ class DeveloperSettingsPresenterTest {
assertThat(initialState.features).isEmpty() assertThat(initialState.features).isEmpty()
assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized) assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.cacheSize).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() cancelAndIgnoreRemainingEvents()
} }
} }
@Test @Test
fun `present - ensures feature list is loaded`() = runTest { fun `present - ensures feature list is loaded`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val presenter = DeveloperSettingsPresenter( val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(), FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(), FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(), FakeClearCacheUseCase(),
rageshakePresenter,
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
@ -66,10 +77,12 @@ class DeveloperSettingsPresenterTest {
@Test @Test
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest { fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val presenter = DeveloperSettingsPresenter( val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(), FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(), FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(), FakeClearCacheUseCase(),
rageshakePresenter,
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
@ -88,11 +101,13 @@ class DeveloperSettingsPresenterTest {
@Test @Test
fun `present - clear cache`() = runTest { fun `present - clear cache`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val clearCacheUseCase = FakeClearCacheUseCase() val clearCacheUseCase = FakeClearCacheUseCase()
val presenter = DeveloperSettingsPresenter( val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(), FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(), FakeComputeCacheSizeUseCase(),
clearCacheUseCase, clearCacheUseCase,
rageshakePresenter,
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()

View file

@ -20,42 +20,45 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat 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.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.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.FakeMatrixClient
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
class PreferencesRootPresenterTest { class PreferencesRootPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val logoutPresenter = DefaultLogoutPreferencePresenter(FakeMatrixClient()) val matrixClient = FakeMatrixClient()
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()) val logoutPresenter = DefaultLogoutPreferencePresenter(matrixClient)
val analyticsPresenter = DefaultAnalyticsPreferencesPresenter(FakeAnalyticsService(), A_BUILD_META)
val presenter = PreferencesRootPresenter( val presenter = PreferencesRootPresenter(
logoutPresenter, logoutPresenter,
rageshakePresenter, matrixClient,
analyticsPresenter, FakeSessionVerificationService(),
A_BUILD_META.buildType BuildType.DEBUG,
FakeVersionFormatter()
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.logoutState.logoutAction).isEqualTo(Async.Uninitialized) assertThat(initialState.myUser).isNull()
assertThat(initialState.analyticsState.isEnabled).isFalse() assertThat(initialState.version).isEqualTo("A Version")
assertThat(initialState.rageshakeState.isEnabled).isTrue() val loadedState = awaitItem()
assertThat(initialState.rageshakeState.isSupported).isTrue() assertThat(loadedState.logoutState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.rageshakeState.sensitivity).isEqualTo(1.0f) assertThat(loadedState.myUser).isEqualTo(
assertThat(initialState.myUser).isEqualTo(Async.Uninitialized) MatrixUser(
assertThat(initialState.showDeveloperSettings).isEqualTo(true) 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 package io.element.android.features.rageshake.api.preferences
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -36,7 +34,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun RageshakePreferencesView( fun RageshakePreferencesView(
state: RageshakePreferencesState, state: RageshakePreferencesState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onOpenRageshake: () -> Unit = {},
) { ) {
fun onSensitivityChanged(sensitivity: Float) { fun onSensitivityChanged(sensitivity: Float) {
state.eventSink(RageshakePreferencesEvents.SetSensitivity(sensitivity = sensitivity)) state.eventSink(RageshakePreferencesEvents.SetSensitivity(sensitivity = sensitivity))
@ -47,13 +44,6 @@ fun RageshakePreferencesView(
} }
Column(modifier = modifier) { 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)) { PreferenceCategory(title = stringResource(id = CommonStrings.settings_rageshake)) {
if (state.isSupported) { if (state.isSupported) {
PreferenceSwitch( PreferenceSwitch(

View file

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

View file

@ -16,8 +16,10 @@
package io.element.android.features.rageshake.impl.bugreport package io.element.android.features.rageshake.impl.bugreport
import android.app.Activity
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
@ -26,8 +28,9 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint 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.di.AppScope
import io.element.android.libraries.ui.strings.CommonStrings
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
class BugReportNode @AssistedInject constructor( class BugReportNode @AssistedInject constructor(
@ -39,10 +42,15 @@ class BugReportNode @AssistedInject constructor(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
val activity = LocalContext.current as? Activity
BugReportView( BugReportView(
state = state, state = state,
modifier = modifier, 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) { is BugReportEvents.SetCanContact -> updateFormState(formState) {
copy(canContact = event.canContact) copy(canContact = event.canContact)
} }
is BugReportEvents.SetSendCrashLog -> updateFormState(formState) {
copy(sendCrashLogs = event.sendCrashlog)
}
is BugReportEvents.SetSendLog -> updateFormState(formState) { is BugReportEvents.SetSendLog -> updateFormState(formState) {
copy(sendLogs = event.sendLog) copy(sendLogs = event.sendLog)
} }
@ -138,7 +135,7 @@ class BugReportPresenter @Inject constructor(
bugReporter.sendBugReport( bugReporter.sendBugReport(
reportType = ReportType.BUG_REPORT, reportType = ReportType.BUG_REPORT,
withDevicesLogs = formState.sendLogs, withDevicesLogs = formState.sendLogs,
withCrashLogs = hasCrashLogs && formState.sendCrashLogs, withCrashLogs = hasCrashLogs && formState.sendLogs,
withKeyRequestHistory = false, withKeyRequestHistory = false,
withScreenshot = formState.sendScreenshot, withScreenshot = formState.sendScreenshot,
theBugDescription = formState.description, theBugDescription = formState.description,

View file

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

View file

@ -17,16 +17,11 @@
package io.element.android.features.rageshake.impl.bugreport package io.element.android.features.rageshake.impl.bugreport
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -35,21 +30,20 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.ImeAction
import androidx.compose.ui.text.input.KeyboardType 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.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import io.element.android.features.rageshake.impl.R import io.element.android.features.rageshake.impl.R
import io.element.android.libraries.architecture.Async 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.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState 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.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
@ -63,8 +57,9 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun BugReportView( fun BugReportView(
state: BugReportState, state: BugReportState,
onDone: () -> Unit,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onDone: () -> Unit = { },
) { ) {
LogCompositions(tag = "Rageshake", msg = "Root") LogCompositions(tag = "Rageshake", msg = "Root")
val eventSink = state.eventSink val eventSink = state.eventSink
@ -75,56 +70,27 @@ fun BugReportView(
} }
return return
} }
Box(
modifier = modifier Box(modifier = modifier) {
.fillMaxSize() PreferenceView(
.systemBarsPadding() title = stringResource(id = CommonStrings.common_report_a_bug),
.imePadding() onBackPressed = onBackPressed
) {
Column(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.padding(horizontal = 16.dp),
) { ) {
val isError = state.sending is Async.Failure
val isFormEnabled = state.sending !is Async.Loading 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( var descriptionFieldState by textFieldState(
stateValue = state.formState.description stateValue = state.formState.description
) )
Column( Spacer(modifier = Modifier.height(16.dp))
// modifier = Modifier.weight(1f), PreferenceRow {
) {
OutlinedTextField( OutlinedTextField(
value = descriptionFieldState, value = descriptionFieldState,
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth()
.padding(top = 16.dp),
enabled = isFormEnabled, enabled = isFormEnabled,
label = { label = {
Text(text = stringResource(id = R.string.screen_bug_report_editor_placeholder)) Text(text = stringResource(id = R.string.screen_bug_report_editor_placeholder))
}, },
supportingText = { supportingText = {
Text(text = stringResource(id = R.string.screen_bug_report_editor_supporting)) Text(text = stringResource(id = R.string.screen_bug_report_editor_description))
}, },
onValueChange = { onValueChange = {
descriptionFieldState = it descriptionFieldState = it
@ -134,35 +100,31 @@ fun BugReportView(
keyboardType = KeyboardType.Text, keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next imeAction = ImeAction.Next
), ),
minLines = 3,
// TODO Error text too short // TODO Error text too short
) )
} }
LabelledCheckbox( Spacer(modifier = Modifier.height(16.dp))
checked = state.formState.sendLogs, PreferenceSwitch(
isChecked = state.formState.sendLogs,
onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) }, onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) },
enabled = isFormEnabled, 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) { PreferenceSwitch(
LabelledCheckbox( isChecked = state.formState.canContact,
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,
onCheckedChange = { eventSink(BugReportEvents.SetCanContact(it)) }, onCheckedChange = { eventSink(BugReportEvents.SetCanContact(it)) },
enabled = isFormEnabled, 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) { if (state.screenshotUri != null) {
LabelledCheckbox( PreferenceSwitch(
checked = state.formState.sendScreenshot, isChecked = state.formState.sendScreenshot,
onCheckedChange = { eventSink(BugReportEvents.SetSendScreenshot(it)) }, onCheckedChange = { eventSink(BugReportEvents.SetSendScreenshot(it)) },
enabled = isFormEnabled, 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) { if (state.formState.sendScreenshot) {
Box( Box(
@ -183,16 +145,19 @@ fun BugReportView(
} }
} }
// Submit // Submit
Button( PreferenceRow {
onClick = { eventSink(BugReportEvents.SendBugReport) }, Button(
enabled = state.submitEnabled, onClick = { eventSink(BugReportEvents.SendBugReport) },
modifier = Modifier enabled = state.submitEnabled,
.fillMaxWidth() modifier = Modifier
.padding(vertical = 32.dp) .fillMaxWidth()
) { .padding(top = 24.dp, bottom = 16.dp)
Text(text = stringResource(id = CommonStrings.action_send)) ) {
Text(text = stringResource(id = CommonStrings.action_send))
}
} }
} }
when (state.sending) { when (state.sending) {
is Async.Loading -> { is Async.Loading -> {
// Indeterminate indicator, to avoid the freeze effect if the connection takes time to initialize. // 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 @Composable
private fun ContentToPreview(state: BugReportState) { 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 { 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 androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.rageshake.api.rageshake.RageshakeDataStore 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.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -45,7 +45,7 @@ class PreferencesRageshakeDataStore @Inject constructor(
override fun isEnabled(): Flow<Boolean> { override fun isEnabled(): Flow<Boolean> {
return store.data.map { prefs -> 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 @Test
fun `present - send logs`() = runTest { fun `present - send logs`() = runTest {
val presenter = BugReportPresenter( val presenter = BugReportPresenter(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,7 +18,6 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
internal val preferenceMinHeightOnlyTitle = 48.dp internal val preferenceMinHeightOnlyTitle = 56.dp
internal val preferenceMinHeight = 64.dp internal val preferenceMinHeight = 56.dp
internal val preferencePaddingHorizontal = 16.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.Icons
import androidx.compose.material.icons.filled.Announcement import androidx.compose.material.icons.filled.Announcement
import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup 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.designsystem.theme.components.Text
import io.element.android.libraries.theme.ElementTheme
@Composable @Composable
fun PreferenceCategory( fun PreferenceCategory(
@ -49,7 +48,7 @@ fun PreferenceCategory(
} }
content() content()
if (showDivider) { if (showDivider) {
Divider() PreferenceDivider()
} }
} }
} }
@ -57,9 +56,14 @@ fun PreferenceCategory(
@Composable @Composable
fun PreferenceCategoryTitle(title: String, modifier: Modifier = Modifier) { fun PreferenceCategoryTitle(title: String, modifier: Modifier = Modifier) {
Text( Text(
modifier = modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp), modifier = modifier.padding(
style = MaterialTheme.typography.titleMedium, top = 20.dp,
color = MaterialTheme.colorScheme.primary, bottom = 8.dp,
start = preferencePaddingHorizontal,
end = preferencePaddingHorizontal,
),
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.materialColors.primary,
text = title, text = title,
) )
} }
@ -85,7 +89,8 @@ private fun ContentToPreview() {
PreferenceSlide( PreferenceSlide(
title = "Slide", title = "Slide",
summary = "Summary", 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.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize 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.material.icons.filled.BugReport
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview 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.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight 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.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.theme.ElementTheme
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
@ -94,15 +90,12 @@ fun PreferenceTopAppBar(
BackButton(onClick = onBackPressed) BackButton(onClick = onBackPressed)
}, },
title = { title = {
Row(verticalAlignment = Alignment.CenterVertically) { Text(
Text( text = title,
fontSize = 16.sp, style = ElementTheme.typography.fontHeadingSmMedium,
fontWeight = FontWeight.SemiBold, maxLines = 1,
text = title, overflow = TextOverflow.Ellipsis
maxLines = 1, )
overflow = TextOverflow.Ellipsis
)
}
} }
) )
@ -129,17 +122,24 @@ private fun ContentToPreview() {
subtitle = "Some other text", subtitle = "Some other text",
icon = Icons.Default.BugReport, icon = Icons.Default.BugReport,
) )
Divider() PreferenceDivider()
PreferenceSwitch( PreferenceSwitch(
title = "Switch", title = "Switch",
icon = Icons.Default.Announcement, icon = Icons.Default.Announcement,
isChecked = true, isChecked = true,
) )
Divider() PreferenceDivider()
PreferenceCheckbox(
title = "Checkbox",
icon = Icons.Default.Announcement,
isChecked = true,
)
PreferenceDivider()
PreferenceSlide( PreferenceSlide(
title = "Slide", title = "Slide",
summary = "Summary", summary = "Summary",
value = 0.75F value = 0.75F,
showIconAreaIfNoIcon = true,
) )
} }
} }

View file

@ -17,7 +17,6 @@
package io.element.android.libraries.designsystem.components.preferences package io.element.android.libraries.designsystem.components.preferences
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize 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.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview 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.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup 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.Slider
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.theme.ElementTheme
@Composable @Composable
fun PreferenceSlide( fun PreferenceSlide(
@ -44,45 +44,41 @@ fun PreferenceSlide(
value: Float, value: Float,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: ImageVector? = null, icon: ImageVector? = null,
showIconAreaIfNoIcon: Boolean = false,
enabled: Boolean = true, enabled: Boolean = true,
summary: String? = null, summary: String? = null,
steps: Int = 0, steps: Int = 0,
onValueChange: (Float) -> Unit = {}, onValueChange: (Float) -> Unit = {},
) { ) {
Box( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight) .defaultMinSize(minHeight = preferenceMinHeight)
.padding(top = preferencePaddingVertical), .padding(vertical = 4.dp, horizontal = preferencePaddingHorizontal),
) { ) {
Row(modifier = Modifier.fillMaxWidth()) { PreferenceIcon(icon = icon, isVisible = showIconAreaIfNoIcon)
PreferenceIcon(icon = icon) Column(
Column( modifier = Modifier
modifier = Modifier .weight(1f),
.weight(1f) ) {
.padding(end = preferencePaddingHorizontal), Text(
) { style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = enabled.toEnabledColor(),
)
summary?.let {
Text( Text(
modifier = Modifier.fillMaxWidth(), style = ElementTheme.typography.fontBodyMdRegular,
style = MaterialTheme.typography.bodyLarge, text = summary,
color = enabled.toEnabledColor(), 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 package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.clickable 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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Announcement 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.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview 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.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup 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.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
import io.element.android.libraries.theme.ElementTheme
@Composable @Composable
fun PreferenceSwitch( fun PreferenceSwitch(
title: String, title: String,
isChecked: Boolean, isChecked: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
subtitle: String? = null,
enabled: Boolean = true, enabled: Boolean = true,
icon: ImageVector? = null, icon: ImageVector? = null,
showIconAreaIfNoIcon: Boolean = false,
onCheckedChange: (Boolean) -> Unit = {}, onCheckedChange: (Boolean) -> Unit = {},
switchAlignment: Alignment.Vertical = Alignment.CenterVertically
) { ) {
Box( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight) .defaultMinSize(minHeight = preferenceMinHeight)
.clickable { onCheckedChange(!isChecked) }, .clickable { onCheckedChange(!isChecked) }
contentAlignment = Alignment.CenterStart .padding(vertical = 4.dp, horizontal = preferencePaddingHorizontal),
verticalAlignment = Alignment.CenterVertically
) { ) {
Row(modifier = Modifier.fillMaxWidth()) { PreferenceIcon(
PreferenceIcon( icon = icon,
modifier = Modifier.padding(vertical = preferencePaddingVertical), enabled = enabled,
icon = icon, isVisible = showIconAreaIfNoIcon
enabled = enabled )
) Column(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
) {
Text( Text(
modifier = Modifier style = ElementTheme.typography.fontBodyLgRegular,
.weight(1f) text = title,
.padding(vertical = preferencePaddingVertical),
style = MaterialTheme.typography.bodyLarge,
color = enabled.toEnabledColor(), 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() { private fun ContentToPreview() {
PreferenceSwitch( PreferenceSwitch(
title = "Switch", title = "Switch",
subtitle = "Subtitle Switch",
icon = Icons.Default.Announcement, icon = Icons.Default.Announcement,
enabled = true, enabled = true,
isChecked = 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.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics import androidx.compose.foundation.progressSemantics
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -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.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Text 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 @Composable
fun PreferenceText( fun PreferenceText(
title: String?, title: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
subtitle: String? = null, subtitle: String? = null,
currentValue: String? = null, currentValue: String? = null,
loadingCurrentValue: Boolean = false, loadingCurrentValue: Boolean = false,
icon: ImageVector? = null, icon: ImageVector? = null,
showIconAreaIfNoIcon: Boolean = false,
tintColor: Color? = null, tintColor: Color? = null,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight
Box(
Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.defaultMinSize(minHeight = minHeight) .defaultMinSize(minHeight = minHeight)
.clickable { onClick() } .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 modifier = Modifier
.fillMaxWidth() .weight(1f)
.padding(vertical = preferencePaddingVertical) .align(Alignment.CenterVertically)
) { ) {
PreferenceIcon(icon = icon, tintColor = tintColor) Text(
Column( style = ElementTheme.typography.fontBodyLgRegular,
modifier = Modifier text = title,
.weight(1f) color = tintColor ?: ElementTheme.materialColors.primary,
.align(Alignment.CenterVertically) )
) { if (subtitle != null) {
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( Text(
modifier = Modifier style = ElementTheme.typography.fontBodyMdRegular,
.align(Alignment.CenterVertically) text = subtitle,
.padding(horizontal = 16.dp), color = tintColor ?: ElementTheme.materialColors.secondary,
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
) )
} }
} }
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, icon = Icons.Default.BugReport,
loadingCurrentValue = true, 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 package io.element.android.libraries.designsystem.components.preferences.components
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -46,14 +46,11 @@ fun PreferenceIcon(
contentDescription = "", contentDescription = "",
tint = tintColor ?: enabled.toSecondaryEnabledColor(), tint = tintColor ?: enabled.toSecondaryEnabledColor(),
modifier = modifier modifier = modifier
.padding(start = 8.dp) .padding(end = 16.dp)
.width(48.dp) .size(24.dp),
.heightIn(max = 48.dp),
) )
} else if (isVisible) { } else if (isVisible) {
Spacer(modifier = modifier.width(56.dp)) Spacer(modifier = modifier.width(40.dp))
} else {
Spacer(modifier = modifier.width(16.dp))
} }
} }

View file

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

View file

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

@ -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 package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column 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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp 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.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark 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.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.getBestName import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.theme.ElementTheme
@Composable @Composable
fun MatrixUserHeader( 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, matrixUser: MatrixUser,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit = {}, // onClick: () -> Unit = {},
) { ) {
Column( Row(
modifier = modifier modifier = modifier
.clickable(onClick = onClick) // .clickable(onClick = onClick)
.fillMaxWidth() .fillMaxWidth()
.padding(all = 16.dp) .padding(horizontal = 16.dp),
.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Avatar( Avatar(
matrixUser.getAvatarData(size = AvatarSize.UserHeader), modifier = Modifier
.padding(vertical = 12.dp),
avatarData = matrixUser.getAvatarData(size = AvatarSize.UserPreference),
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.width(16.dp))
// Name Column(
Text( modifier = Modifier.weight(1f)
fontSize = 18.sp, ) {
fontWeight = FontWeight.SemiBold, // Name
text = matrixUser.getBestName(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.primary,
)
// Id
if (matrixUser.displayName.isNullOrEmpty().not()) {
Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = matrixUser.userId.value, text = matrixUser.getBestName(),
color = MaterialTheme.colorScheme.secondary,
fontSize = 14.sp,
maxLines = 1, 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 version https://git-lfs.github.com/spec/v1
oid sha256:e3ccec7a26ef11d01dfed40d56937c9bf9c1411680b2ff7338cf10f9541a3880 oid sha256:940c3ac11da74a6eb734085c468050040c2d31056b6d8de516e49ddb0058c9ee
size 23871 size 23412

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:381032750ffe5cbe710ef0535424eaffc95e68ae728cfb048ca51e646627e52d oid sha256:fb77df9e072715ed947537e4474d504b5b5d533bfe9cd888362a421fea5b53b5
size 31366 size 44068

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:381032750ffe5cbe710ef0535424eaffc95e68ae728cfb048ca51e646627e52d oid sha256:fb77df9e072715ed947537e4474d504b5b5d533bfe9cd888362a421fea5b53b5
size 31366 size 44068

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:16e2259bc7287d7f69e757165a37f4b00c83198e220a735344a242aa0a6780c0 oid sha256:004a25de04aac1ca9cbbad77581e0b52a6f8fba30aa2e64c03a791c54b297d5a
size 34785 size 48875

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:16e2259bc7287d7f69e757165a37f4b00c83198e220a735344a242aa0a6780c0 oid sha256:004a25de04aac1ca9cbbad77581e0b52a6f8fba30aa2e64c03a791c54b297d5a
size 34785 size 48875

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9b67102fcc5195f76c2e654de9169a9309a4d5f0ca8ccbdf2a0cd3e7d3c9a62c oid sha256:b8417eee1585f6ec9a29ea1e827415f8b108f88688af9c3b9f5740f499cd1df5
size 48713 size 35126

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:365e1631bee71a673120786e428dadefea38842b94b119cbd5b5b3a038a69e7e oid sha256:3dc2cf5f31b6183171a5b92197f30bc03ef8693be50d1a6e9db4860a670f7440
size 47870 size 34516

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2d0e4f89015644572b38bc2621f5056c4098621f60fe39c524b4b138ee22314c oid sha256:050cb885911e011f25e302fee3ec060d74fae70b7b2405927fffdf06ca3b0fee
size 53147 size 37187

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6ba3c9b1a3f9d387a9fdc4e8a190a25b614b7b5c02bc21cf43c6cc1795b1e5d0 oid sha256:dc612a9d9b82f54373fb92e5d4519e331c315391afb52067b2e22c6e0379f3cc
size 53193 size 37336

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0f3a406b87274bffe778e244ead901913218b38fa3647aaab1abc3a95fc63ba5 oid sha256:79de0ef9c06b6e95d02f6a3dc27d10d741623de162a5597822a50692f2cd28a8
size 14979 size 13089

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:37858c668f8024a21164a8d15af51e71c69cc03eb9f20613ecfc65dcf543818d oid sha256:d38b0802eab518e2546629197b418d7a1b0369d921b199dc59797bae0636ed94
size 13297 size 12435

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b oid sha256:cd987ad48b569d423d1c37743d0e0b862600cf166f357c997435bad6841e3d4d
size 4457 size 6001

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a4269d4349997ea6d056c7e65a3307e7ad7de620093d3304be30041a2f8a0795 oid sha256:8a56c4aa5117ec8ce4d63ee679443b88eaddf84795b21f4b61429ae10ddfd2fe
size 14235 size 12831

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:41d45ef3fd17be71f42982f457f03bfe4d88efb2aabe4df69d3b3a76f0dba7b1 oid sha256:b3b82fb2d33d5abf66035fd26b7eab778f4b6aa67f29b05e0b252a9cb57ce365
size 13393 size 12957

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b oid sha256:61a986107eceb0f6eb11b0807946a84a0c61887fc5c1f5232c70e180c2f124c5
size 4457 size 5793

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:022f325b03a7a115fc4490766e561dfc65471aaee59e76bd8234dca263a7ce41 oid sha256:f6399530b992aff076af9a57a1267aa9ef8347b5d2a693e153ddc1607e25ba41
size 22525 size 18524

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:40832c445fb109f2d9177e3aadf981fab832124497ca6b9689cb838f0e5c4386 oid sha256:20feb0126811049b238303dd11824cc65aadc5b7256e08cbe2c604452fbd712a
size 21088 size 15109

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:84c51965accb84017f200813f24112aebd5dbaeba15140bcb12d5a52f68ed294 oid sha256:e631b7635ec2d9f636c5c0b5174c7f914a76e1fed843eb209805289023b4fe75
size 23379 size 19347

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b504a78119e3cdbd410d6c221ebddcddcc2fe8a74cadf8fa2da0c4397b2cd368 oid sha256:1338c3951801ff9872765b20508f37b15132c61685a3d79f80471f3939d8f249
size 21769 size 16072

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