Merge branch 'develop' into feature/fga/room_detail_factory_improvement

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

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?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

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

View file

@ -59,6 +59,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_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

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -32,5 +32,6 @@ interface PreferencesEntryPoint : FeatureEntryPoint {
interface Callback : Plugin { 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

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

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

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

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,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

View file

@ -20,7 +20,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.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

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

View file

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

View file

@ -16,23 +16,19 @@
package io.element.android.libraries.matrix.ui.components 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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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