Merge branch 'develop' into feature/fga/room_detail_factory_improvement
This commit is contained in:
commit
14d4480c0a
204 changed files with 1838 additions and 822 deletions
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -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>
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 = "",
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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 |
|
|
@ -32,5 +32,6 @@ interface PreferencesEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenBugReport()
|
fun onOpenBugReport()
|
||||||
|
fun onVerifyClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>,
|
||||||
|
)
|
||||||
|
|
@ -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 = "",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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(),
|
||||||
|
)
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:63f37eb39f73c5c4e371dc3c2f6b8c1ec5e58acaee666af235b8b15a79c749fa
|
||||||
|
size 15970
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue