Merge pull request #82 from vector-im/bma/viewCoverage

Add screenshot test and update documentation.
This commit is contained in:
Benoit Marty 2023-02-17 09:52:26 +01:00 committed by GitHub
commit 2ba44c70fc
403 changed files with 2863 additions and 601 deletions

View file

@ -22,7 +22,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
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.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
@ -31,14 +34,14 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable @Composable
internal fun ShowkaseButton( internal fun ShowkaseButton(
isVisible: Boolean, isVisible: Boolean,
onClick: () -> Unit,
onCloseClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit = {},
onCloseClicked: () -> Unit = {},
) { ) {
if (isVisible) { if (isVisible) {
Button( Button(
modifier = modifier modifier = modifier
.padding(top = 32.dp, start = 16.dp), .padding(top = 32.dp),
onClick = onClick onClick = onClick
) { ) {
Text(text = "Showkase Browser") Text(text = "Showkase Browser")
@ -53,3 +56,16 @@ internal fun ShowkaseButton(
} }
} }
} }
@Preview
@Composable
internal fun ShowkaseButtonLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun ShowkaseButtonDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
ShowkaseButton(isVisible = true)
}

View file

@ -19,29 +19,23 @@ package io.element.android.x.root
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.rageshake.bugreport.BugReportPresenter
import io.element.android.features.rageshake.crash.ui.CrashDetectionPresenter import io.element.android.features.rageshake.crash.ui.CrashDetectionPresenter
import io.element.android.features.rageshake.detection.RageshakeDetectionPresenter import io.element.android.features.rageshake.detection.RageshakeDetectionPresenter
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject import javax.inject.Inject
class RootPresenter @Inject constructor( class RootPresenter @Inject constructor(
private val bugReportPresenter: BugReportPresenter,
private val crashDetectionPresenter: CrashDetectionPresenter, private val crashDetectionPresenter: CrashDetectionPresenter,
private val rageshakeDetectionPresenter: RageshakeDetectionPresenter, private val rageshakeDetectionPresenter: RageshakeDetectionPresenter,
) : Presenter<RootState> { ) : Presenter<RootState> {
@Composable @Composable
override fun present(): RootState { override fun present(): RootState {
val isBugReportVisible = rememberSaveable {
mutableStateOf(false)
}
val isShowkaseButtonVisible = rememberSaveable { val isShowkaseButtonVisible = rememberSaveable {
mutableStateOf(true) mutableStateOf(true)
} }
val rageshakeDetectionState = rageshakeDetectionPresenter.present() val rageshakeDetectionState = rageshakeDetectionPresenter.present()
val crashDetectionState = crashDetectionPresenter.present() val crashDetectionState = crashDetectionPresenter.present()
val bugReportState = bugReportPresenter.present()
fun handleEvent(event: RootEvents) { fun handleEvent(event: RootEvents) {
when (event) { when (event) {
@ -50,11 +44,9 @@ class RootPresenter @Inject constructor(
} }
return RootState( return RootState(
isBugReportVisible = isBugReportVisible.value,
isShowkaseButtonVisible = isShowkaseButtonVisible.value, isShowkaseButtonVisible = isShowkaseButtonVisible.value,
rageshakeDetectionState = rageshakeDetectionState, rageshakeDetectionState = rageshakeDetectionState,
crashDetectionState = crashDetectionState, crashDetectionState = crashDetectionState,
bugReportState = bugReportState,
eventSink = ::handleEvent eventSink = ::handleEvent
) )
} }

View file

@ -17,16 +17,13 @@
package io.element.android.x.root package io.element.android.x.root
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import io.element.android.features.rageshake.bugreport.BugReportState
import io.element.android.features.rageshake.crash.ui.CrashDetectionState import io.element.android.features.rageshake.crash.ui.CrashDetectionState
import io.element.android.features.rageshake.detection.RageshakeDetectionState import io.element.android.features.rageshake.detection.RageshakeDetectionState
@Stable @Stable
data class RootState( data class RootState(
val isBugReportVisible: Boolean,
val isShowkaseButtonVisible: Boolean, val isShowkaseButtonVisible: Boolean,
val rageshakeDetectionState: RageshakeDetectionState, val rageshakeDetectionState: RageshakeDetectionState,
val crashDetectionState: CrashDetectionState, val crashDetectionState: CrashDetectionState,
val bugReportState: BugReportState,
val eventSink: (RootEvents) -> Unit val eventSink: (RootEvents) -> Unit
) )

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.x.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.rageshake.crash.ui.aCrashDetectionState
import io.element.android.features.rageshake.detection.aRageshakeDetectionState
open class RootStateProvider : PreviewParameterProvider<RootState> {
override val values: Sequence<RootState>
get() = sequenceOf(
aRootState().copy(
isShowkaseButtonVisible = true,
rageshakeDetectionState = aRageshakeDetectionState().copy(showDialog = false),
crashDetectionState = aCrashDetectionState().copy(crashDetected = true),
),
aRootState().copy(
isShowkaseButtonVisible = true,
rageshakeDetectionState = aRageshakeDetectionState().copy(showDialog = true),
crashDetectionState = aCrashDetectionState().copy(crashDetected = false),
)
)
}
fun aRootState() = RootState(
isShowkaseButtonVisible = false,
rageshakeDetectionState = aRageshakeDetectionState(),
crashDetectionState = aCrashDetectionState(),
eventSink = {}
)

View file

@ -24,10 +24,15 @@ 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.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.rageshake.crash.ui.CrashDetectionEvents import io.element.android.features.rageshake.crash.ui.CrashDetectionEvents
import io.element.android.features.rageshake.crash.ui.CrashDetectionView import io.element.android.features.rageshake.crash.ui.CrashDetectionView
import io.element.android.features.rageshake.detection.RageshakeDetectionEvents import io.element.android.features.rageshake.detection.RageshakeDetectionEvents
import io.element.android.features.rageshake.detection.RageshakeDetectionView import io.element.android.features.rageshake.detection.RageshakeDetectionView
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.tests.uitests.openShowkase import io.element.android.tests.uitests.openShowkase
import io.element.android.x.component.ShowkaseButton import io.element.android.x.component.ShowkaseButton
@ -68,3 +73,18 @@ fun RootView(
) )
} }
} }
@Preview
@Composable
internal fun RootLightPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreviewLight { ContentToPreview(rootState) }
@Preview
@Composable
internal fun RootDarkPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreviewDark { ContentToPreview(rootState) }
@Composable
private fun ContentToPreview(rootState: RootState) {
RootView(rootState) {
Text("Children")
}
}

View file

@ -22,12 +22,10 @@ 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.rageshake.bugreport.BugReportPresenter
import io.element.android.features.rageshake.crash.ui.CrashDetectionPresenter import io.element.android.features.rageshake.crash.ui.CrashDetectionPresenter
import io.element.android.features.rageshake.detection.RageshakeDetectionPresenter import io.element.android.features.rageshake.detection.RageshakeDetectionPresenter
import io.element.android.features.rageshake.preferences.RageshakePreferencesPresenter import io.element.android.features.rageshake.preferences.RageshakePreferencesPresenter
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -58,17 +56,11 @@ class RootPresenterTest {
} }
} }
private fun TestScope.createPresenter(): RootPresenter { private fun createPresenter(): RootPresenter {
val crashDataStore = FakeCrashDataStore() val crashDataStore = FakeCrashDataStore()
val rageshakeDataStore = FakeRageshakeDataStore() val rageshakeDataStore = FakeRageshakeDataStore()
val rageshake = FakeRageShake() val rageshake = FakeRageShake()
val screenshotHolder = FakeScreenshotHolder() val screenshotHolder = FakeScreenshotHolder()
val bugReportPresenter = BugReportPresenter(
bugReporter = FakeBugReporter(),
crashDataStore = crashDataStore,
screenshotHolder = screenshotHolder,
appCoroutineScope = this,
)
val crashDetectionPresenter = CrashDetectionPresenter( val crashDetectionPresenter = CrashDetectionPresenter(
crashDataStore = crashDataStore crashDataStore = crashDataStore
) )
@ -81,7 +73,6 @@ class RootPresenterTest {
) )
) )
return RootPresenter( return RootPresenter(
bugReportPresenter = bugReportPresenter,
crashDetectionPresenter = crashDetectionPresenter, crashDetectionPresenter = crashDetectionPresenter,
rageshakeDetectionPresenter = rageshakeDetectionPresenter, rageshakeDetectionPresenter = rageshakeDetectionPresenter,
) )

View file

@ -151,6 +151,12 @@ allprojects {
} }
} }
allprojects {
tasks.withType<Test> {
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
}
}
allprojects { allprojects {
apply(plugin = "kover") apply(plugin = "kover")
} }
@ -178,6 +184,9 @@ koverMerged {
"*ComposableSingletons$*", "*ComposableSingletons$*",
"*_AssistedFactory_Impl*", "*_AssistedFactory_Impl*",
"*BuildConfig", "*BuildConfig",
// Generated by Showkase
"*Ioelementandroid*PreviewKt$*",
"*Ioelementandroid*PreviewKt",
// Other // Other
// We do not cover Nodes (normally covered by maestro, but code coverage is not computed with maestro) // We do not cover Nodes (normally covered by maestro, but code coverage is not computed with maestro)
"*Node", "*Node",
@ -196,10 +205,12 @@ koverMerged {
name = "Global minimum code coverage." name = "Global minimum code coverage."
target = kotlinx.kover.api.VerificationTarget.ALL target = kotlinx.kover.api.VerificationTarget.ALL
bound { bound {
minValue = 45 minValue = 55
// Setting a max value, so that if coverage is bigger, it means that we have to change minValue. // Setting a max value, so that if coverage is bigger, it means that we have to change minValue.
maxValue = 50 // For instance if we have minValue = 25 and maxValue = 30, and current code coverage is now 37.32%, update
counter = kotlinx.kover.api.CounterType.LINE // minValue to 35 and maxValue to 40.
maxValue = 60
counter = kotlinx.kover.api.CounterType.INSTRUCTION
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
} }
} }
@ -217,7 +228,7 @@ koverMerged {
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
} }
} }
// Rule to ensure that coverage of State is sufficient. // Rule to ensure that coverage of States is sufficient.
rule { rule {
name = "Check code coverage of states" name = "Check code coverage of states"
target = kotlinx.kover.api.VerificationTarget.CLASS target = kotlinx.kover.api.VerificationTarget.CLASS
@ -230,5 +241,19 @@ koverMerged {
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
} }
} }
// Rule to ensure that coverage of Views is sufficient (deactivated for now).
rule {
name = "Check code coverage of views"
target = kotlinx.kover.api.VerificationTarget.CLASS
overrideClassFilter {
includes += "*ViewKt"
}
bound {
// TODO Update this value, for now there are too many missing tests.
minValue = 0
counter = kotlinx.kover.api.CounterType.INSTRUCTION
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
}
}
} }
} }

View file

@ -12,10 +12,11 @@
* [Application](#application) * [Application](#application)
* [Jetpack Compose](#jetpack-compose) * [Jetpack Compose](#jetpack-compose)
* [Global architecture](#global-architecture) * [Global architecture](#global-architecture)
* [Template](#template) * [Template and naming](#template-and-naming)
* [Push](#push) * [Push](#push)
* [Dependencies management](#dependencies-management) * [Dependencies management](#dependencies-management)
* [Test](#test) * [Test](#test)
* [Code coverage](#code-coverage)
* [Other points](#other-points) * [Other points](#other-points)
* [Logging](#logging) * [Logging](#logging)
* [Rageshake](#rageshake) * [Rageshake](#rageshake)
@ -130,7 +131,7 @@ About Preview
Main libraries and frameworks used in this application: Main libraries and frameworks used in this application:
- Navigation state with [Appyx](https://bumble-tech.github.io/appyx/) - Navigation state with [Appyx](https://bumble-tech.github.io/appyx/). Please watch [this video](https://www.droidcon.com/2022/11/15/model-driven-navigation-with-appyx-from-zero-to-hero/) to learn more about Appyx!
- DI: [Dagger](https://dagger.dev/) and [Anvil](https://github.com/square/anvil) - DI: [Dagger](https://dagger.dev/) and [Anvil](https://github.com/square/anvil)
- Reactive State management with Compose runtime and [Molecule](https://github.com/cashapp/molecule) - Reactive State management with Compose runtime and [Molecule](https://github.com/cashapp/molecule)
@ -143,14 +144,17 @@ Here are the main points:
3. Presenters are also compose first, and have a single `present(): State` method. It's using the power of compose-runtime/compiler. 3. Presenters are also compose first, and have a single `present(): State` method. It's using the power of compose-runtime/compiler.
4. The point of connection between a `View` and a `Presenter` is a `Node`. 4. The point of connection between a `View` and a `Presenter` is a `Node`.
5. A `Node` is also responsible for managing Dagger components if any. 5. A `Node` is also responsible for managing Dagger components if any.
6. A `ParentNode` has some child `Node` and only know about them. 6. A `ParentNode` has some children `Node` and only know about them.
7. This is a single activity full compose application. The `MainActivity` is responsible for holding and configuring the `RootNode`. 7. This is a single activity full compose application. The `MainActivity` is responsible for holding and configuring the `RootNode`.
8. There is no more needs for Android Architecture Component ViewModel as configuration change should be handled by Composable if needed. 8. There is no more needs for Android Architecture Component ViewModel as configuration change should be handled by Composable if needed.
#### Template #### Template and naming
(TODO: This is coming) There is a template module to easily start a new feature. When creating a new module, you can just copy paste the template. It is located [here](../features/template).
There is a template module to easily start a new feature. When creating a new module, you can just copy paste the template.
For the naming rules, please follow what is being currently used in the template module.
Note that naming of files and classes is important, since those names are used to set up code coverage rules. For instance, presenters MUST have a suffix `Presenter`,states MUST have a suffix `State`, etc. Also we want to have a common naming along all the modules.
### Push ### Push
@ -172,17 +176,41 @@ All the dependencies (including android artifact, gradle plugin, etc.) should be
Some dependency, mainly because they are not shared can be declared in `build.gradle.kts` files. Some dependency, mainly because they are not shared can be declared in `build.gradle.kts` files.
[Dependabot](https://github.com/dependabot) is set up on the project. This tool will automatically create Pull Request to upgrade our dependencies one by one. [Dependabot](https://github.com/dependabot) is set up on the project. This tool will automatically create Pull Request to upgrade our dependencies one by one.
**Note** Dependabot does not support yet Gradle verrsion catalog. This is tracked by [this issue](https://github.com/dependabot/dependabot-core/issues/3121). **Note** Dependabot does not support yet Gradle version catalog. This is tracked by [this issue](https://github.com/dependabot/dependabot-core/issues/3121).
### Test ### Test
We have 3 tests frameworks in place: We have 3 tests frameworks in place, and this should be sufficient to guarantee a good code coverage and limit regressions hopefully:
- Maestro to test the global usage of the application. See the related [documentation](../.maestro/README.md). - Maestro to test the global usage of the application. See the related [documentation](../.maestro/README.md).
- Combination of [Showkase](https://github.com/airbnb/Showkase) and [Paparazzi](https://github.com/cashapp/paparazzi), to test UI pixel perfect. To add test, just add `@Preview` for the composable you are adding. See the related [documentation](screenshot_testing.md). - Combination of [Showkase](https://github.com/airbnb/Showkase) and [Paparazzi](https://github.com/cashapp/paparazzi), to test UI pixel perfect. To add test, just add `@Preview` for the composable you are adding. See the related [documentation](screenshot_testing.md) and see in the template the file [TemplateView.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt). We create PreviewProvider to provide different states. See for instance the file [TemplateStateProvider.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateStateProvider.kt)
- Tests on presenter with Molecule and [Turbine](https://github.com/cashapp/turbine) (TODO this is coming) - Tests on presenter with [Molecule](https://github.com/cashapp/molecule) and [Turbine](https://github.com/cashapp/turbine). See in the template the class [TemplatePresenterTests](../features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt).
**Note** For now we want to avoid using mock (such as *mockk*), because this should be note necessary. **Note** For now we want to avoid using class mocking (with library such as *mockk*), because this should be not necessary. We prefer to create Fake implementation of our interfaces. Mocking can be used to mock Android framework classes though, such as `Bitmap` for instance.
### Code coverage
[kover](https://github.com/Kotlin/kotlinx-kover) is used to compute code coverage. Only have unit tests can produce code coverage result. Running Maestro does not participate to the code coverage results.
Kover configuration is defined in the main [build.gradle.kts](../build.gradle.kts) file.
To compute the code coverage, run:
```bash
./gradlew koverMergedReport
```
and open the Html report: [../build/reports/kover/merged/html/index.html](../build/reports/kover/merged/html/index.html)
To ensure that the code coverage threshold are OK, you can run
```bash
./gradlew koverMergedVerify
```
Note that the CI performs this check on every pull requests.
Also, if the rule `Global minimum code coverage.` is in error because code coverage is `> maxValue`, `minValue` and `maxValue` can be updated for this rule in the file [build.gradle.kts](../build.gradle.kts) (you will see further instructions there).
### Other points ### Other points

View file

@ -19,9 +19,9 @@ package io.element.android.features.login.changeserver
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
data class ChangeServerState( data class ChangeServerState(
val homeserver: String = "", val homeserver: String,
val changeServerAction: Async<Unit> = Async.Uninitialized, val changeServerAction: Async<Unit>,
val eventSink: (ChangeServerEvents) -> Unit = {}, val eventSink: (ChangeServerEvents) -> Unit,
) { ) {
val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Async.Loading val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Async.Loading
} }

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.changeserver
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> {
override val values: Sequence<ChangeServerState>
get() = sequenceOf(
aChangeServerState(),
aChangeServerState().copy(homeserver = "matrix.org"),
aChangeServerState().copy(homeserver = "matrix.org", changeServerAction = Async.Loading()),
aChangeServerState().copy(homeserver = "invalid.org", changeServerAction = Async.Failure(Throwable("An error"))),
aChangeServerState().copy(homeserver = "matrix.org", changeServerAction = Async.Success(Unit)),
)
}
fun aChangeServerState() = ChangeServerState(
homeserver = "",
changeServerAction = Async.Uninitialized,
eventSink = {}
)

View file

@ -42,17 +42,18 @@ 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.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.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.element.android.features.login.R import io.element.android.features.login.R
import io.element.android.features.login.error.changeServerError import io.element.android.features.login.error.changeServerError
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.VectorIcon
import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.components.form.textFieldState
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.Button import io.element.android.libraries.designsystem.theme.components.Button
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.Icon
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
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
@ -90,12 +91,13 @@ fun ChangeServerView(
shape = RoundedCornerShape(32.dp) shape = RoundedCornerShape(32.dp)
) )
) { ) {
VectorIcon( Icon(
modifier = Modifier modifier = Modifier
.align(Alignment.Center) .align(Alignment.Center)
.size(width = 48.dp, height = 48.dp), .size(width = 48.dp, height = 48.dp),
// TODO Update with design input // TODO Update with design input
resourceId = R.drawable.ic_baseline_dataset_24, resourceId = R.drawable.ic_baseline_dataset_24,
contentDescription = "",
) )
} }
Text( Text(
@ -179,15 +181,15 @@ fun ChangeServerView(
@Preview @Preview
@Composable @Composable
fun ChangeServerViewLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun ChangeServerViewLightPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview @Preview
@Composable @Composable
fun ChangeServerViewDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview(state: ChangeServerState) {
ChangeServerView( ChangeServerView(state = state)
state = ChangeServerState(homeserver = "matrix.org"),
)
} }

View file

@ -221,16 +221,16 @@ fun LoginRootScreen(
@Preview @Preview
@Composable @Composable
fun LoginRootScreenLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun LoginRootScreenLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun LoginRootScreenDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun LoginRootScreenDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {
LoginRootScreen( LoginRootScreen(
state = LoginRootState( state = aLoginRootState().copy(
homeserver = "matrix.org", homeserver = "matrix.org",
), ),
) )

View file

@ -21,10 +21,10 @@ import io.element.android.libraries.matrix.core.SessionId
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
data class LoginRootState( data class LoginRootState(
val homeserver: String = "", val homeserver: String,
val loggedInState: LoggedInState = LoggedInState.NotLoggedIn, val loggedInState: LoggedInState,
val formState: LoginFormState = LoginFormState.Default, val formState: LoginFormState,
val eventSink: (LoginRootEvents) -> Unit = {} val eventSink: (LoginRootEvents) -> Unit
) { ) {
val submitEnabled = val submitEnabled =
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState != LoggedInState.LoggingIn formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState != LoggedInState.LoggingIn

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.login.root
fun aLoginRootState() = LoginRootState(
homeserver = "",
loggedInState = LoggedInState.NotLoggedIn,
formState = LoginFormState.Default,
eventSink = {}
)

View file

@ -92,13 +92,13 @@ fun LogoutPreferenceContent(
@Preview @Preview
@Composable @Composable
fun LogoutPreferenceViewLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun LogoutPreferenceViewLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun LogoutPreferenceViewDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun LogoutPreferenceViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {
LogoutPreferenceView(LogoutPreferenceState()) LogoutPreferenceView(aLogoutPreferenceState())
} }

View file

@ -19,6 +19,6 @@ package io.element.android.features.logout
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
data class LogoutPreferenceState( data class LogoutPreferenceState(
val logoutAction: Async<Unit> = Async.Uninitialized, val logoutAction: Async<Unit>,
val eventSink: (LogoutPreferenceEvents) -> Unit = {}, val eventSink: (LogoutPreferenceEvents) -> Unit,
) )

View file

@ -14,24 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.designsystem.components package io.element.android.features.logout
import androidx.compose.material3.LocalContentColor import io.element.android.libraries.architecture.Async
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
@Composable fun aLogoutPreferenceState() = LogoutPreferenceState(
fun VectorIcon( logoutAction = Async.Uninitialized,
resourceId: Int, eventSink = {}
modifier: Modifier = Modifier, )
tint: Color = LocalContentColor.current,
) {
androidx.compose.material3.Icon(
painter = painterResource(id = resourceId),
contentDescription = null,
modifier = modifier,
tint = tint
)
}

View file

@ -67,7 +67,8 @@ class MessagesPresenter @Inject constructor(
LaunchedEffect(syncUpdateFlow) { LaunchedEffect(syncUpdateFlow) {
roomAvatar.value = roomAvatar.value =
AvatarData( AvatarData(
name = room.bestName, id = room.roomId.value,
name = room.name,
url = room.avatarUrl, url = room.avatarUrl,
size = AvatarSize.SMALL size = AvatarSize.SMALL
) )

View file

@ -0,0 +1,52 @@
/*
* 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.messages
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.actionlist.anActionListState
import io.element.android.features.messages.textcomposer.aMessageComposerState
import io.element.android.features.messages.timeline.aTimelineItemContent
import io.element.android.features.messages.timeline.aTimelineItemList
import io.element.android.features.messages.timeline.aTimelineState
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.RoomId
import io.element.android.libraries.textcomposer.MessageComposerMode
open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
override val values: Sequence<MessagesState>
get() = sequenceOf(
aMessagesState(),
)
}
fun aMessagesState() = MessagesState(
roomId = RoomId("!id"),
roomName = "Room name",
roomAvatar = AvatarData("!id", "Room name"),
composerState = aMessageComposerState().copy(
text = StableCharSequence("Hello"),
isFullScreen = false,
mode = MessageComposerMode.Normal("Hello"),
),
timelineState = aTimelineState().copy(
timelineItems = aTimelineItemList(aTimelineItemContent()),
hasMoreToLoad = false,
),
actionListState = anActionListState(),
eventSink = {}
)

View file

@ -49,6 +49,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight 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.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.element.android.features.messages.actionlist.ActionListEvents import io.element.android.features.messages.actionlist.ActionListEvents
@ -59,6 +61,8 @@ import io.element.android.features.messages.timeline.TimelineView
import io.element.android.features.messages.timeline.model.TimelineItem import io.element.android.features.messages.timeline.model.TimelineItem
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.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -72,7 +76,7 @@ import timber.log.Timber
fun MessagesView( fun MessagesView(
state: MessagesState, state: MessagesState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onBackPressed: () -> Unit, onBackPressed: () -> Unit = {},
) { ) {
LogCompositions(tag = "MessagesScreen", msg = "Root") LogCompositions(tag = "MessagesScreen", msg = "Root")
val itemActionsBottomSheetState = rememberModalBottomSheetState( val itemActionsBottomSheetState = rememberModalBottomSheetState(
@ -197,6 +201,20 @@ fun MessagesViewTopBar(
) )
} }
} }
) )
} }
@Preview
@Composable
internal fun MessagesViewLightPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
internal fun MessagesViewDarkPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: MessagesState) {
MessagesView(state)
}

View file

@ -26,7 +26,6 @@ data class ActionListState(
val target: Target, val target: Target,
val eventSink: (ActionListEvents) -> Unit, val eventSink: (ActionListEvents) -> Unit,
) { ) {
sealed interface Target { sealed interface Target {
object None : Target object None : Target
data class Loading(val messageEvent: TimelineItem.MessageEvent) : Target data class Loading(val messageEvent: TimelineItem.MessageEvent) : Target

View file

@ -0,0 +1,47 @@
/*
* 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.messages.actionlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.features.messages.timeline.aMessageEvent
import kotlinx.collections.immutable.persistentListOf
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
override val values: Sequence<ActionListState>
get() = sequenceOf(
anActionListState(),
anActionListState().copy(target = ActionListState.Target.Loading(aMessageEvent())),
anActionListState().copy(
target = ActionListState.Target.Success(
messageEvent = aMessageEvent(),
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Copy,
TimelineItemAction.Edit,
TimelineItemAction.Redact,
)
)
)
)
}
fun anActionListState() = ActionListState(
target = ActionListState.Target.None,
eventSink = {}
)

View file

@ -28,7 +28,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ListItem import androidx.compose.material.ListItem
import androidx.compose.material.LocalContentColor
import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text import androidx.compose.material.Text
@ -38,11 +37,14 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.features.messages.actionlist.model.TimelineItemAction import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.features.messages.timeline.model.TimelineItem import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.libraries.designsystem.components.VectorIcon import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -115,13 +117,14 @@ private fun SheetContent(
text = { text = {
Text( Text(
text = action.title, text = action.title,
color = if (action.destructive) MaterialTheme.colorScheme.error else Color.Unspecified, color = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
) )
}, },
icon = { icon = {
VectorIcon( Icon(
resourceId = action.icon, resourceId = action.icon,
tint = if (action.destructive) MaterialTheme.colorScheme.error else LocalContentColor.current, contentDescription = "",
tint = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
) )
} }
) )
@ -130,3 +133,18 @@ private fun SheetContent(
} }
} }
} }
@Preview
@Composable
fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: ActionListState) {
SheetContent(state)
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2022 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.messages.textcomposer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.textcomposer.MessageComposerMode
open class MessageComposerStateProvider : PreviewParameterProvider<MessageComposerState> {
override val values: Sequence<MessageComposerState>
get() = sequenceOf(
aMessageComposerState(),
)
}
fun aMessageComposerState() = MessageComposerState(
text = StableCharSequence(""),
isFullScreen = false,
mode = MessageComposerMode.Normal(content = ""),
eventSink = {}
)

View file

@ -18,6 +18,10 @@ package io.element.android.features.messages.textcomposer
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.PreviewParameter
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.textcomposer.TextComposer import io.element.android.libraries.textcomposer.TextComposer
@ -55,3 +59,18 @@ fun MessageComposerView(
modifier = modifier modifier = modifier
) )
} }
@Preview
@Composable
internal fun MessageComposerViewLightPreview(@PreviewParameter(MessageComposerStateProvider::class) state: MessageComposerState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
internal fun MessageComposerViewDarkPreview(@PreviewParameter(MessageComposerStateProvider::class) state: MessageComposerState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: MessageComposerState) {
MessageComposerView(state)
}

View file

@ -20,7 +20,7 @@ import androidx.recyclerview.widget.DiffUtil
import io.element.android.features.messages.timeline.diff.CacheInvalidator import io.element.android.features.messages.timeline.diff.CacheInvalidator
import io.element.android.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback import io.element.android.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback
import io.element.android.features.messages.timeline.model.AggregatedReaction import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItem import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.TimelineItemReactions import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.features.messages.timeline.model.content.TimelineItemContent import io.element.android.features.messages.timeline.model.content.TimelineItemContent
@ -156,7 +156,8 @@ class TimelineItemsFactory @Inject constructor(
val senderDisplayName = room.userDisplayName(currentSender).getOrNull() val senderDisplayName = room.userDisplayName(currentSender).getOrNull()
val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull() val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull()
val senderAvatarData = AvatarData( val senderAvatarData = AvatarData(
name = senderDisplayName ?: currentSender, id = currentSender,
name = senderDisplayName,
url = senderAvatarUrl, url = senderAvatarUrl,
size = AvatarSize.SMALL size = AvatarSize.SMALL
) )
@ -233,7 +234,7 @@ class TimelineItemsFactory @Inject constructor(
currentTimelineItem: MatrixTimelineItem.Event, currentTimelineItem: MatrixTimelineItem.Event,
timelineItems: List<MatrixTimelineItem>, timelineItems: List<MatrixTimelineItem>,
index: Int index: Int
): MessagesItemGroupPosition { ): TimelineItemGroupPosition {
val prevTimelineItem = val prevTimelineItem =
timelineItems.getOrNull(index - 1) as? MatrixTimelineItem.Event timelineItems.getOrNull(index - 1) as? MatrixTimelineItem.Event
val nextTimelineItem = val nextTimelineItem =
@ -243,10 +244,10 @@ class TimelineItemsFactory @Inject constructor(
val nextSender = nextTimelineItem?.event?.sender() val nextSender = nextTimelineItem?.event?.sender()
return when { return when {
previousSender != currentSender && nextSender == currentSender -> MessagesItemGroupPosition.First previousSender != currentSender && nextSender == currentSender -> TimelineItemGroupPosition.First
previousSender == currentSender && nextSender == currentSender -> MessagesItemGroupPosition.Middle previousSender == currentSender && nextSender == currentSender -> TimelineItemGroupPosition.Middle
previousSender == currentSender && nextSender != currentSender -> MessagesItemGroupPosition.Last previousSender == currentSender && nextSender != currentSender -> TimelineItemGroupPosition.Last
else -> MessagesItemGroupPosition.None else -> TimelineItemGroupPosition.None
} }
} }
} }

View file

@ -0,0 +1,100 @@
/*
* 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.messages.timeline
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemTextContent
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.EventId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
fun aTimelineState() = TimelineState(
timelineItems = persistentListOf(),
hasMoreToLoad = false,
highlightedEventId = null,
eventSink = {}
)
internal fun aTimelineItemList(content: TimelineItemContent): ImmutableList<TimelineItem> {
return persistentListOf(
// 3 items (First Middle Last) with isMine = false
aMessageEvent(
isMine = false,
content = content,
groupPosition = TimelineItemGroupPosition.Last
),
aMessageEvent(
isMine = false,
content = content,
groupPosition = TimelineItemGroupPosition.Middle
),
aMessageEvent(
isMine = false,
content = content,
groupPosition = TimelineItemGroupPosition.First
),
// 3 items (First Middle Last) with isMine = true
aMessageEvent(
isMine = true,
content = content,
groupPosition = TimelineItemGroupPosition.Last
),
aMessageEvent(
isMine = true,
content = content,
groupPosition = TimelineItemGroupPosition.Middle
),
aMessageEvent(
isMine = true,
content = content,
groupPosition = TimelineItemGroupPosition.First
),
)
}
internal fun aMessageEvent(
isMine: Boolean = false,
content: TimelineItemContent = aTimelineItemContent(),
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First
): TimelineItem.MessageEvent {
return TimelineItem.MessageEvent(
id = EventId(Math.random().toString()),
senderId = "@senderId",
senderAvatar = AvatarData("@senderId", "sender"),
content = content,
reactionsState = TimelineItemReactions(
persistentListOf(
AggregatedReaction("👍", "1")
)
),
isMine = isMine,
senderDisplayName = "sender",
groupPosition = groupPosition,
)
}
internal fun aTimelineItemContent(): TimelineItemContent {
return TimelineItemTextContent(
body = "Text",
htmlDocument = null
)
}

View file

@ -54,6 +54,7 @@ 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.zIndex import androidx.compose.ui.zIndex
import io.element.android.features.messages.timeline.components.BubbleState
import io.element.android.features.messages.timeline.components.MessageEventBubble import io.element.android.features.messages.timeline.components.MessageEventBubble
import io.element.android.features.messages.timeline.components.TimelineItemEncryptedView import io.element.android.features.messages.timeline.components.TimelineItemEncryptedView
import io.element.android.features.messages.timeline.components.TimelineItemImageView import io.element.android.features.messages.timeline.components.TimelineItemImageView
@ -61,13 +62,9 @@ import io.element.android.features.messages.timeline.components.TimelineItemReac
import io.element.android.features.messages.timeline.components.TimelineItemRedactedView import io.element.android.features.messages.timeline.components.TimelineItemRedactedView
import io.element.android.features.messages.timeline.components.TimelineItemTextView import io.element.android.features.messages.timeline.components.TimelineItemTextView
import io.element.android.features.messages.timeline.components.TimelineItemUnknownView import io.element.android.features.messages.timeline.components.TimelineItemUnknownView
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItem import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.TimelineItemGroupPositionProvider
import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.features.messages.timeline.model.content.MessagesTimelineItemContentProvider
import io.element.android.features.messages.timeline.model.content.TimelineItemContent import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemContentProvider
import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
@ -81,10 +78,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.PairCombinedPreviewParameter
import io.element.android.libraries.matrix.core.EventId
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -200,10 +194,12 @@ fun MessageEventRow(
) )
} }
MessageEventBubble( MessageEventBubble(
groupPosition = messageEvent.groupPosition, state = BubbleState(
isMine = messageEvent.isMine, groupPosition = messageEvent.groupPosition,
isMine = messageEvent.isMine,
isHighlighted = isHighlighted,
),
interactionSource = interactionSource, interactionSource = interactionSource,
isHighlighted = isHighlighted,
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
modifier = Modifier modifier = Modifier
@ -355,78 +351,22 @@ internal fun TimelineLoadingMoreIndicator() {
@Preview @Preview
@Composable @Composable
fun LoginRootScreenLightPreview( fun LoginRootScreenLightPreview(
@PreviewParameter(MessagesTimelineItemContentProvider::class) content: TimelineItemContent @PreviewParameter(TimelineItemContentProvider::class) content: TimelineItemContent
) = ElementPreviewLight { ContentToPreview(content) } ) = ElementPreviewLight { ContentToPreview(content) }
@Preview @Preview
@Composable @Composable
fun LoginRootScreenDarkPreview( fun LoginRootScreenDarkPreview(
@PreviewParameter(MessagesTimelineItemContentProvider::class) content: TimelineItemContent @PreviewParameter(TimelineItemContentProvider::class) content: TimelineItemContent
) = ElementPreviewDark { ContentToPreview(content) } ) = ElementPreviewDark { ContentToPreview(content) }
@Composable @Composable
private fun ContentToPreview(content: TimelineItemContent) { private fun ContentToPreview(content: TimelineItemContent) {
val timelineItems = persistentListOf( val timelineItems = aTimelineItemList(content)
// 3 items (First Middle Last) with isMine = false
createMessageEvent(
isMine = false,
content = content,
groupPosition = MessagesItemGroupPosition.Last
),
createMessageEvent(
isMine = false,
content = content,
groupPosition = MessagesItemGroupPosition.Middle
),
createMessageEvent(
isMine = false,
content = content,
groupPosition = MessagesItemGroupPosition.First
),
// 3 items (First Middle Last) with isMine = true
createMessageEvent(
isMine = true,
content = content,
groupPosition = MessagesItemGroupPosition.Last
),
createMessageEvent(
isMine = true,
content = content,
groupPosition = MessagesItemGroupPosition.Middle
),
createMessageEvent(
isMine = true,
content = content,
groupPosition = MessagesItemGroupPosition.First
),
)
TimelineView( TimelineView(
state = TimelineState( state = aTimelineState().copy(
timelineItems = timelineItems, timelineItems = timelineItems,
hasMoreToLoad = true, hasMoreToLoad = true,
highlightedEventId = null,
eventSink = {}
) )
) )
} }
private fun createMessageEvent(
isMine: Boolean,
content: TimelineItemContent,
groupPosition: MessagesItemGroupPosition
): TimelineItem {
return TimelineItem.MessageEvent(
id = EventId(Math.random().toString()),
senderId = "senderId",
senderAvatar = AvatarData("sender"),
content = content,
reactionsState = TimelineItemReactions(
persistentListOf(
AggregatedReaction("👍", "1")
)
),
isMine = isMine,
senderDisplayName = "sender",
groupPosition = groupPosition,
)
}

View file

@ -0,0 +1,27 @@
/*
* 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.messages.timeline.components
import androidx.compose.runtime.Stable
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
@Stable
data class BubbleState(
val groupPosition: TimelineItemGroupPosition,
val isMine: Boolean,
val isHighlighted: Boolean,
)

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.components
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
open class BubbleStateProvider : PreviewParameterProvider<BubbleState> {
override val values: Sequence<BubbleState>
get() = sequenceOf(
TimelineItemGroupPosition.First,
TimelineItemGroupPosition.Middle,
TimelineItemGroupPosition.Last,
).map { groupPosition ->
sequenceOf(false, true).map { isMine ->
sequenceOf(false, true).map { isHighlighted ->
BubbleState(groupPosition, isMine = isMine, isHighlighted = isHighlighted)
}
}
.flatten()
}
.flatten()
}
fun aBubbleState() = BubbleState(
groupPosition = TimelineItemGroupPosition.First,
isMine = false,
isHighlighted = false,
)

View file

@ -19,16 +19,25 @@ package io.element.android.features.messages.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
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.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
@ -37,33 +46,31 @@ private val BUBBLE_RADIUS = 16.dp
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun MessageEventBubble( fun MessageEventBubble(
groupPosition: MessagesItemGroupPosition, state: BubbleState,
isMine: Boolean,
interactionSource: MutableInteractionSource, interactionSource: MutableInteractionSource,
isHighlighted: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
onLongClick: () -> Unit = {}, onLongClick: () -> Unit = {},
content: @Composable () -> Unit = {}, content: @Composable () -> Unit = {},
) { ) {
fun bubbleShape(): Shape { fun bubbleShape(): Shape {
return when (groupPosition) { return when (state.groupPosition) {
MessagesItemGroupPosition.First -> if (isMine) { TimelineItemGroupPosition.First -> if (state.isMine) {
RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS) RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS)
} else { } else {
RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp) RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp)
} }
MessagesItemGroupPosition.Middle -> if (isMine) { TimelineItemGroupPosition.Middle -> if (state.isMine) {
RoundedCornerShape(BUBBLE_RADIUS, 0.dp, 0.dp, BUBBLE_RADIUS) RoundedCornerShape(BUBBLE_RADIUS, 0.dp, 0.dp, BUBBLE_RADIUS)
} else { } else {
RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp) RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp)
} }
MessagesItemGroupPosition.Last -> if (isMine) { TimelineItemGroupPosition.Last -> if (state.isMine) {
RoundedCornerShape(BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS) RoundedCornerShape(BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS)
} else { } else {
RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS) RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS)
} }
MessagesItemGroupPosition.None -> TimelineItemGroupPosition.None ->
RoundedCornerShape( RoundedCornerShape(
BUBBLE_RADIUS, BUBBLE_RADIUS,
BUBBLE_RADIUS, BUBBLE_RADIUS,
@ -74,17 +81,17 @@ fun MessageEventBubble(
} }
fun Modifier.offsetForItem(): Modifier { fun Modifier.offsetForItem(): Modifier {
return if (isMine) { return if (state.isMine) {
offset(y = -(12.dp)) offset(y = -(12.dp))
} else { } else {
offset(x = 20.dp, y = -(12.dp)) offset(x = 20.dp, y = -(12.dp))
} }
} }
val backgroundBubbleColor = if (isHighlighted) { val backgroundBubbleColor = if (state.isHighlighted) {
ElementTheme.colors.messageHighlightedBackground ElementTheme.colors.messageHighlightedBackground
} else { } else {
if (isMine) { if (state.isMine) {
ElementTheme.colors.messageFromMeBackground ElementTheme.colors.messageFromMeBackground
} else { } else {
ElementTheme.colors.messageFromOtherBackground ElementTheme.colors.messageFromOtherBackground
@ -107,3 +114,31 @@ fun MessageEventBubble(
content = content content = content
) )
} }
@Preview
@Composable
internal fun MessageEventBubbleLightPreview(@PreviewParameter(BubbleStateProvider::class) state: BubbleState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
internal fun MessageEventBubbleDarkPreview(@PreviewParameter(BubbleStateProvider::class) state: BubbleState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: BubbleState) {
// Due to y offset, surround with a Box
Box(
modifier = Modifier
.size(width = 240.dp, height = 64.dp)
.padding(8.dp),
contentAlignment = Alignment.CenterStart,
) {
MessageEventBubble(
state = state,
interactionSource = MutableInteractionSource(),
) {
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
}
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2022 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.messages.timeline.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
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.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.AggregatedReactionProvider
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.surfaceVariant,
border = BorderStroke(2.dp, MaterialTheme.colorScheme.background),
shape = RoundedCornerShape(corner = CornerSize(12.dp)),
) {
Row(
modifier = Modifier.padding(vertical = 5.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// TODO `reaction.isHighlighted` is not used.
Text(text = reaction.key, fontSize = 12.sp)
Spacer(modifier = Modifier.width(4.dp))
Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp)
}
}
}
@Preview
@Composable
internal fun MessagesReactionButtonLightPreview(@PreviewParameter(AggregatedReactionProvider::class) reaction: AggregatedReaction) =
ElementPreviewLight { ContentToPreview(reaction) }
@Preview
@Composable
internal fun MessagesReactionButtonDarkPreview(@PreviewParameter(AggregatedReactionProvider::class) reaction: AggregatedReaction) =
ElementPreviewDark { ContentToPreview(reaction) }
@Composable
private fun ContentToPreview(reaction: AggregatedReaction) {
MessagesReactionButton(reaction)
}

View file

@ -20,7 +20,11 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning
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 io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import org.matrix.rustcomponents.sdk.EncryptedMessage
@Composable @Composable
fun TimelineItemEncryptedView( fun TimelineItemEncryptedView(
@ -34,3 +38,22 @@ fun TimelineItemEncryptedView(
modifier = modifier modifier = modifier
) )
} }
@Preview
@Composable
internal fun TimelineItemEncryptedViewLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun TimelineItemEncryptedViewDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
TimelineItemEncryptedView(
content = TimelineItemEncryptedContent(
encryptedMessage = EncryptedMessage.Unknown,
)
)
}

View file

@ -28,9 +28,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContentProvider
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
@Composable @Composable
fun TimelineItemImageView( fun TimelineItemImageView(
@ -48,7 +54,7 @@ fun TimelineItemImageView(
.aspectRatio(content.aspectRatio), .aspectRatio(content.aspectRatio),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
var isLoading = rememberSaveable(content.imageMeta) { mutableStateOf(true) } val isLoading = rememberSaveable(content.imageMeta) { mutableStateOf(true) }
val context = LocalContext.current val context = LocalContext.current
val model = ImageRequest.Builder(context) val model = ImageRequest.Builder(context)
.data(content.imageMeta) .data(content.imageMeta)
@ -57,9 +63,24 @@ fun TimelineItemImageView(
AsyncImage( AsyncImage(
model = model, model = model,
contentDescription = null, contentDescription = null,
placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), placeholder = debugPlaceholderBackground(ColorPainter(MaterialTheme.colorScheme.surfaceVariant)),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
onSuccess = { isLoading.value = false }, onSuccess = { isLoading.value = false },
) )
} }
} }
@Preview
@Composable
internal fun TimelineItemImageViewLightPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) =
ElementPreviewLight { ContentToPreview(content) }
@Preview
@Composable
internal fun TimelineItemImageViewDarkPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) =
ElementPreviewDark { ContentToPreview(content) }
@Composable
private fun ContentToPreview(content: TimelineItemImageContent) {
TimelineItemImageView(content)
}

View file

@ -65,11 +65,11 @@ fun TimelineItemInformativeView(
@Preview @Preview
@Composable @Composable
fun MatrixUserRowLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun TimelineItemInformativeViewLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun MatrixUserRowDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun TimelineItemInformativeViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {

View file

@ -16,24 +16,15 @@
package io.element.android.features.messages.timeline.components package io.element.android.features.messages.timeline.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.TimelineItemReactions import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.features.messages.timeline.model.aTimelineItemReactions
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable @Composable
fun TimelineItemReactionsView( fun TimelineItemReactionsView(
@ -52,21 +43,19 @@ fun TimelineItemReactionsView(
} }
} }
@Preview
@Composable @Composable
fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier) { internal fun TimelineItemReactionsViewLightPreview() =
Surface( ElementPreviewLight { ContentToPreview() }
modifier = modifier,
color = MaterialTheme.colorScheme.surfaceVariant, @Preview
border = BorderStroke(2.dp, MaterialTheme.colorScheme.background), @Composable
shape = RoundedCornerShape(corner = CornerSize(12.dp)), internal fun TimelineItemReactionsViewDarkPreview() =
) { ElementPreviewDark { ContentToPreview() }
Row(
modifier = Modifier.padding(vertical = 5.dp, horizontal = 8.dp), @Composable
verticalAlignment = Alignment.CenterVertically private fun ContentToPreview() {
) { TimelineItemReactionsView(
Text(text = reaction.key, fontSize = 12.sp) reactionsState = aTimelineItemReactions()
Spacer(modifier = Modifier.width(4.dp)) )
Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp)
}
}
} }

View file

@ -20,7 +20,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete 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.tooling.preview.Preview
import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable @Composable
fun TimelineItemRedactedView( fun TimelineItemRedactedView(
@ -34,3 +37,18 @@ fun TimelineItemRedactedView(
modifier = modifier modifier = modifier
) )
} }
@Preview
@Composable
internal fun TimelineItemRedactedViewLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun TimelineItemRedactedViewDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
TimelineItemRedactedView(TimelineItemRedactedContent)
}

View file

@ -27,11 +27,16 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.core.text.util.LinkifyCompat import androidx.core.text.util.LinkifyCompat
import io.element.android.features.messages.timeline.components.html.HtmlDocument import io.element.android.features.messages.timeline.components.html.HtmlDocument
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContent import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContentProvider
import io.element.android.libraries.designsystem.LinkColor import io.element.android.libraries.designsystem.LinkColor
import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable @Composable
fun TimelineItemTextView( fun TimelineItemTextView(
@ -91,3 +96,19 @@ private fun String.linkify(
) )
} }
} }
@Preview
@Composable
internal fun TimelineItemTextViewLightPreview(@PreviewParameter(TimelineItemTextBasedContentProvider::class) content: TimelineItemTextBasedContent) =
ElementPreviewLight { ContentToPreview(content) }
@Preview
@Composable
internal fun TimelineItemTextViewDarkPreview(@PreviewParameter(TimelineItemTextBasedContentProvider::class) content: TimelineItemTextBasedContent) =
ElementPreviewDark { ContentToPreview(content) }
@Composable
fun ContentToPreview(content: TimelineItemTextBasedContent) {
TimelineItemTextView(content, MutableInteractionSource())
}

View file

@ -20,7 +20,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
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 io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable @Composable
fun TimelineItemUnknownView( fun TimelineItemUnknownView(
@ -34,3 +37,18 @@ fun TimelineItemUnknownView(
modifier = modifier modifier = modifier
) )
} }
@Preview
@Composable
internal fun TimelineItemUnknownViewLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun TimelineItemUnknownViewDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
TimelineItemUnknownView(TimelineItemUnknownContent)
}

View file

@ -0,0 +1,54 @@
/*
* 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.messages.timeline.components.html
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
open class DocumentProvider : PreviewParameterProvider<Document> {
override val values: Sequence<Document>
get() = sequenceOf(
"text",
"<strong>Strong</strong>",
"<b>Bold</b>",
"<i>Italic</i>",
// FIXME This does not work
"<b><i>Bold then italic</i></b>",
// FIXME This does not work
"<i><b>Italic then bold</b></i>",
"<em>em</em>",
"<unknown>unknown</unknown>",
// FIXME `br` is not rendered correctly in the Preview.
"Line 1<br/>Line 2",
"<code>code</code>",
"<del>del</del>",
"<h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3><h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6><h7>Heading 7</h7>",
"<a href=\"https://matrix.org\">link</a>",
"<p>paragraph</p>",
"<p>paragraph 1</p><p>paragraph 2</p>",
"<ol><li>ol item 1</li><li>ol item 2</li></ol>",
"<ol><li><i>ol item 1 italic</i></li><li><b>ol item 2 bold</b></li></ol>",
"<ul><li>ul item 1</li><li>ul item 2</li></ul>",
"<blockquote>blockquote</blockquote>",
// TODO Find a way to make is work with `pre`. For now there is an error with
// jsoup: java.lang.NoSuchMethodError: 'org.jsoup.nodes.Element org.jsoup.nodes.Element.firstElementChild()'
// "<pre>pre</pre>",
"<mx-reply><blockquote><a href=\\\"https://matrix.to/#/!roomId/\$eventId?via=matrix.org\\\">In reply to</a> " +
"<a href=\\\"https://matrix.to/#/@alice:matrix.org\\\">@alice:matrix.org</a><br>original message</blockquote></mx-reply>reply",
).map { Jsoup.parse(it) }
}

View file

@ -42,11 +42,15 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import io.element.android.libraries.designsystem.LinkColor import io.element.android.libraries.designsystem.LinkColor
import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.permalink.PermalinkData import io.element.android.libraries.matrix.permalink.PermalinkData
@ -99,7 +103,10 @@ private fun HtmlBody(
when (val node = nodes.next()) { when (val node = nodes.next()) {
is TextNode -> { is TextNode -> {
if (!node.isBlank) { if (!node.isBlank) {
Text(text = node.text()) Text(
text = node.text(),
color = MaterialTheme.colorScheme.primary,
)
} }
} }
is Element -> { is Element -> {
@ -139,7 +146,7 @@ private fun HtmlBody(
} }
private fun Element.isInline(): Boolean { private fun Element.isInline(): Boolean {
return when (normalName()) { return when (tagName().lowercase()) {
"del" -> true "del" -> true
"mx-reply" -> false "mx-reply" -> false
else -> !isBlock else -> !isBlock
@ -156,7 +163,7 @@ private fun HtmlBlock(
) { ) {
val blockModifier = modifier val blockModifier = modifier
.padding(top = 4.dp) .padding(top = 4.dp)
when (element.normalName()) { when (element.tagName().lowercase()) {
"p" -> HtmlParagraph( "p" -> HtmlParagraph(
paragraph = element, paragraph = element,
modifier = blockModifier, modifier = blockModifier,
@ -230,7 +237,7 @@ private fun HtmlPreformatted(
pre: Element, pre: Element,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val isCode = pre.firstElementChild()?.normalName() == "code" val isCode = pre.firstElementChild()?.tagName()?.lowercase() == "code"
val backgroundColor = val backgroundColor =
if (isCode) MaterialTheme.colorScheme.codeBackground() else Color.Unspecified if (isCode) MaterialTheme.colorScheme.codeBackground() else Color.Unspecified
Box( Box(
@ -241,6 +248,7 @@ private fun HtmlPreformatted(
Text( Text(
text = pre.wholeText(), text = pre.wholeText(),
style = TextStyle(fontFamily = FontFamily.Monospace), style = TextStyle(fontFamily = FontFamily.Monospace),
color = MaterialTheme.colorScheme.primary,
) )
} }
} }
@ -305,7 +313,7 @@ private fun HtmlHeading(
onTextClicked: () -> Unit = {}, onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {}, onTextLongClicked: () -> Unit = {},
) { ) {
val style = when (heading.normalName()) { val style = when (heading.tagName().lowercase()) {
"h1" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 30.sp) "h1" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 30.sp)
"h2" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 26.sp) "h2" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 26.sp)
"h3" -> MaterialTheme.typography.headlineMedium.copy(fontSize = 22.sp) "h3" -> MaterialTheme.typography.headlineMedium.copy(fontSize = 22.sp)
@ -361,7 +369,7 @@ private fun HtmlMxReply(
} }
} }
is Element -> { is Element -> {
when (blockquoteNode.normalName()) { when (blockquoteNode.tagName().lowercase()) {
"br" -> { "br" -> {
append('\n') append('\n')
} }
@ -483,7 +491,7 @@ private fun AnnotatedString.Builder.appendInlineChildrenElements(
} }
private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors: ColorScheme) { private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors: ColorScheme) {
when (element.normalName()) { when (element.tagName().lowercase()) {
"br" -> { "br" -> {
append('\n') append('\n')
} }
@ -502,6 +510,7 @@ private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors
appendInlineChildrenElements(element.childNodes(), colors) appendInlineChildrenElements(element.childNodes(), colors)
} }
} }
"i",
"em" -> { "em" -> {
withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) { withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
appendInlineChildrenElements(element.childNodes(), colors) appendInlineChildrenElements(element.childNodes(), colors)
@ -567,3 +576,18 @@ private fun HtmlText(
onLongClick = onLongClick onLongClick = onLongClick
) )
} }
@Preview
@Composable
internal fun HtmlDocumentLightPreview(@PreviewParameter(DocumentProvider::class) document: Document) =
ElementPreviewLight { ContentToPreview(document) }
@Preview
@Composable
internal fun HtmlDocumentDarkPreview(@PreviewParameter(DocumentProvider::class) document: Document) =
ElementPreviewDark { ContentToPreview(document) }
@Composable
private fun ContentToPreview(document: Document) {
HtmlDocument(document, MutableInteractionSource())
}

View file

@ -0,0 +1,23 @@
/*
* 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.messages.timeline.model
data class AggregatedReaction(
val key: String,
val count: String,
val isHighlighted: Boolean = false
)

View file

@ -0,0 +1,35 @@
/*
* 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.messages.timeline.model
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class AggregatedReactionProvider : PreviewParameterProvider<AggregatedReaction> {
override val values: Sequence<AggregatedReaction>
get() = sequenceOf(
anAggregatedReaction(),
anAggregatedReaction().copy(count = "88"),
anAggregatedReaction().copy(isHighlighted = true),
anAggregatedReaction().copy(count = "88", isHighlighted = true),
)
}
fun anAggregatedReaction() = AggregatedReaction(
key = "👍",
count = "1", // TODO Why is it a String?
isHighlighted = false,
)

View file

@ -35,7 +35,7 @@ sealed interface TimelineItem {
val content: TimelineItemContent, val content: TimelineItemContent,
val sentTime: String = "", val sentTime: String = "",
val isMine: Boolean = false, val isMine: Boolean = false,
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None, val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
val reactionsState: TimelineItemReactions val reactionsState: TimelineItemReactions
) : TimelineItem { ) : TimelineItem {

View file

@ -17,14 +17,13 @@
package io.element.android.features.messages.timeline.model package io.element.android.features.messages.timeline.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@Immutable @Immutable
sealed interface MessagesItemGroupPosition { sealed interface TimelineItemGroupPosition {
object First : MessagesItemGroupPosition object First : TimelineItemGroupPosition
object Middle : MessagesItemGroupPosition object Middle : TimelineItemGroupPosition
object Last : MessagesItemGroupPosition object Last : TimelineItemGroupPosition
object None : MessagesItemGroupPosition object None : TimelineItemGroupPosition
fun isNew(): Boolean = when (this) { fun isNew(): Boolean = when (this) {
First, None -> true First, None -> true
@ -32,11 +31,3 @@ sealed interface MessagesItemGroupPosition {
} }
} }
internal class TimelineItemGroupPositionProvider : PreviewParameterProvider<MessagesItemGroupPosition> {
override val values = sequenceOf(
MessagesItemGroupPosition.First,
MessagesItemGroupPosition.Middle,
MessagesItemGroupPosition.Last,
MessagesItemGroupPosition.None,
)
}

View file

@ -0,0 +1,28 @@
/*
* 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.messages.timeline.model
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
internal class TimelineItemGroupPositionProvider : PreviewParameterProvider<TimelineItemGroupPosition> {
override val values = sequenceOf(
TimelineItemGroupPosition.First,
TimelineItemGroupPosition.Middle,
TimelineItemGroupPosition.Last,
TimelineItemGroupPosition.None,
)
}

View file

@ -21,9 +21,3 @@ import kotlinx.collections.immutable.ImmutableList
data class TimelineItemReactions( data class TimelineItemReactions(
val reactions: ImmutableList<AggregatedReaction> val reactions: ImmutableList<AggregatedReaction>
) )
data class AggregatedReaction(
val key: String,
val count: String,
val isHighlighted: Boolean = false
)

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.model
import kotlinx.collections.immutable.toPersistentList
fun aTimelineItemReactions() = TimelineItemReactions(
// Use values from AggregatedReactionProvider
reactions = AggregatedReactionProvider().values.toPersistentList()
)

View file

@ -16,30 +16,4 @@
package io.element.android.features.messages.timeline.model.content package io.element.android.features.messages.timeline.model.content
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import org.matrix.rustcomponents.sdk.EncryptedMessage
sealed interface TimelineItemContent sealed interface TimelineItemContent
class MessagesTimelineItemContentProvider : PreviewParameterProvider<TimelineItemContent> {
override val values = sequenceOf(
TimelineItemEmoteContent(
body = "Emote",
htmlDocument = null
),
TimelineItemEncryptedContent(
encryptedMessage = EncryptedMessage.Unknown
),
// TODO MessagesTimelineItemImageContent(),
TimelineItemNoticeContent(
body = "Notice",
htmlDocument = null
),
TimelineItemRedactedContent,
TimelineItemTextContent(
body = "Text",
htmlDocument = null
),
TimelineItemUnknownContent,
)
}

View file

@ -0,0 +1,67 @@
/*
* 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.messages.timeline.model.content
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import org.jsoup.Jsoup
import org.matrix.rustcomponents.sdk.EncryptedMessage
class TimelineItemContentProvider : PreviewParameterProvider<TimelineItemContent> {
override val values = sequenceOf(
aTimelineItemEmoteContent(),
aTimelineItemEncryptedContent(),
// TODO MessagesTimelineItemImageContent(),
aTimelineItemNoticeContent(),
aTimelineItemRedactedContent(),
aTimelineItemTextContent(),
aTimelineItemUnknownContent(),
)
}
class TimelineItemTextBasedContentProvider : PreviewParameterProvider<TimelineItemTextBasedContent> {
override val values = sequenceOf(
aTimelineItemEmoteContent(),
aTimelineItemEmoteContent().copy(htmlDocument = Jsoup.parse("Emote")),
aTimelineItemNoticeContent(),
aTimelineItemNoticeContent().copy(htmlDocument = Jsoup.parse("Notice")),
aTimelineItemTextContent(),
aTimelineItemTextContent().copy(htmlDocument = Jsoup.parse("Text")),
)
}
fun aTimelineItemEmoteContent() = TimelineItemEmoteContent(
body = "Emote",
htmlDocument = null
)
fun aTimelineItemEncryptedContent() = TimelineItemEncryptedContent(
encryptedMessage = EncryptedMessage.Unknown
)
fun aTimelineItemNoticeContent() = TimelineItemNoticeContent(
body = "Notice",
htmlDocument = null
)
fun aTimelineItemRedactedContent() = TimelineItemRedactedContent
fun aTimelineItemTextContent() = TimelineItemTextContent(
body = "Text",
htmlDocument = null
)
fun aTimelineItemUnknownContent() = TimelineItemUnknownContent

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.model.content
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.media.MediaResolver
open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineItemImageContent> {
override val values: Sequence<TimelineItemImageContent>
get() = sequenceOf(
aTimelineItemImageContent(),
aTimelineItemImageContent().copy(aspectRatio = 1.0f),
aTimelineItemImageContent().copy(aspectRatio = 1.5f),
)
}
fun aTimelineItemImageContent() = TimelineItemImageContent(
body = "a body",
imageMeta = MediaResolver.Meta(source = null, kind = MediaResolver.Kind.Content),
blurhash = null,
aspectRatio = 0.5f,
)

View file

@ -170,7 +170,7 @@ private fun aMessageEvent(
id = AN_EVENT_ID, id = AN_EVENT_ID,
senderId = A_USER_ID.value, senderId = A_USER_ID.value,
senderDisplayName = A_USER_NAME, senderDisplayName = A_USER_NAME,
senderAvatar = AvatarData(), senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
content = content, content = content,
sentTime = "", sentTime = "",
isMine = isMine, isMine = isMine,

View file

@ -168,7 +168,7 @@ private fun aMessageEvent(
id = AN_EVENT_ID, id = AN_EVENT_ID,
senderId = A_USER_ID.value, senderId = A_USER_ID.value,
senderDisplayName = A_USER_NAME, senderDisplayName = A_USER_NAME,
senderAvatar = AvatarData(), senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
content = content, content = content,
sentTime = "", sentTime = "",
isMine = isMine, isMine = isMine,

View file

@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
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.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
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
@ -37,12 +38,15 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.HorizontalPagerIndicator import com.google.accompanist.pager.HorizontalPagerIndicator
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
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
@ -150,6 +154,7 @@ fun OnBoardingPage(
.padding(8.dp), .padding(8.dp),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
fontSize = 24.sp, fontSize = 24.sp,
) )
Text( Text(
@ -158,7 +163,23 @@ fun OnBoardingPage(
.fillMaxWidth() .fillMaxWidth()
.align(CenterHorizontally), .align(CenterHorizontally),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.primary,
) )
} }
} }
} }
@Preview
@Composable
internal fun OnBoardingScreenLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun OnBoardingScreenDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
OnBoardingScreen()
}

View file

@ -0,0 +1,27 @@
/*
* 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.root
import io.element.android.features.logout.aLogoutPreferenceState
import io.element.android.features.rageshake.preferences.aRageshakePreferencesState
import io.element.android.libraries.architecture.Async
fun aPreferencesRootState() = PreferencesRootState(
logoutState = aLogoutPreferenceState(),
rageshakeState = aRageshakePreferencesState(),
myUser = Async.Uninitialized
)

View file

@ -20,15 +20,16 @@ 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 io.element.android.features.logout.LogoutPreferenceState import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.logout.LogoutPreferenceView import io.element.android.features.logout.LogoutPreferenceView
import io.element.android.features.preferences.user.UserPreferences import io.element.android.features.preferences.user.UserPreferences
import io.element.android.features.rageshake.preferences.RageshakePreferencesState
import io.element.android.features.rageshake.preferences.RageshakePreferencesView import io.element.android.features.rageshake.preferences.RageshakePreferencesView
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
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.matrix.ui.components.MatrixUserProvider
import io.element.android.libraries.matrix.ui.model.MatrixUser
import io.element.android.libraries.ui.strings.R as StringR import io.element.android.libraries.ui.strings.R as StringR
@Composable @Composable
@ -58,18 +59,15 @@ fun PreferencesRootView(
@Preview @Preview
@Composable @Composable
fun PreferencesRootViewLightPreview() = ElementPreviewLight { ContentToPreview() } fun PreferencesRootViewLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
ElementPreviewLight { ContentToPreview(matrixUser) }
@Preview @Preview
@Composable @Composable
fun PreferencesRootViewDarkPreview() = ElementPreviewDark { ContentToPreview() } fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
ElementPreviewDark { ContentToPreview(matrixUser) }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview(matrixUser: MatrixUser) {
val state = PreferencesRootState( PreferencesRootView(aPreferencesRootState().copy(myUser = Async.Success(matrixUser)))
logoutState = LogoutPreferenceState(),
rageshakeState = RageshakePreferencesState(),
myUser = Async.Uninitialized
)
PreferencesRootView(state)
} }

View file

@ -20,9 +20,14 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height 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.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.matrix.ui.components.MatrixUserHeader import io.element.android.libraries.matrix.ui.components.MatrixUserHeader
import io.element.android.libraries.matrix.ui.components.MatrixUserWithNullProvider
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
@Composable @Composable
@ -38,3 +43,22 @@ fun UserPreferences(
) )
} }
} }
@Preview
@Composable
internal fun UserPreferencesLightPreview(@PreviewParameter(MatrixUserWithNullProvider::class) matrixUser: MatrixUser?) =
ElementPreviewLight { ContentToPreview(matrixUser) }
@Preview
@Composable
internal fun UserPreferencesDarkPreview(@PreviewParameter(MatrixUserWithNullProvider::class) matrixUser: MatrixUser?) =
ElementPreviewDark { ContentToPreview(matrixUser) }
@Composable
private fun ContentToPreview(matrixUser: MatrixUser?) {
if (matrixUser == null) {
UserPreferences(Async.Uninitialized)
} else {
UserPreferences(Async.Success(matrixUser))
}
}

View file

@ -21,12 +21,12 @@ import io.element.android.libraries.architecture.Async
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
data class BugReportState( data class BugReportState(
val formState: BugReportFormState = BugReportFormState.Default, val formState: BugReportFormState,
val hasCrashLogs: Boolean = false, val hasCrashLogs: Boolean,
val screenshotUri: String? = null, val screenshotUri: String?,
val sendingProgress: Float = 0F, val sendingProgress: Float,
val sending: Async<Unit> = Async.Uninitialized, val sending: Async<Unit>,
val eventSink: (BugReportEvents) -> Unit = {} val eventSink: (BugReportEvents) -> Unit
) { ) {
val submitEnabled = val submitEnabled =
formState.description.length > 10 && sending !is Async.Loading formState.description.length > 10 && sending !is Async.Loading

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2022 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.rageshake.bugreport
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
open class BugReportStateProvider : PreviewParameterProvider<BugReportState> {
override val values: Sequence<BugReportState>
get() = sequenceOf(
aBugReportState(),
aBugReportState().copy(
formState = BugReportFormState.Default.copy(
description = "A long enough description",
sendScreenshot = true,
),
hasCrashLogs = true,
screenshotUri = "aUri"
),
aBugReportState().copy(sending = Async.Loading()),
aBugReportState().copy(sending = Async.Success(Unit)),
)
}
fun aBugReportState() = BugReportState(
formState = BugReportFormState.Default,
hasCrashLogs = false,
screenshotUri = null,
sendingProgress = 0F,
sending = Async.Uninitialized,
eventSink = {}
)

View file

@ -40,6 +40,7 @@ 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.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.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
@ -50,6 +51,7 @@ 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.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.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
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.OutlinedTextField import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
@ -107,7 +109,7 @@ fun BugReportView(
.padding(horizontal = 16.dp, vertical = 16.dp), .padding(horizontal = 16.dp, vertical = 16.dp),
fontSize = 16.sp, fontSize = 16.sp,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
var descriptionFieldState by textFieldState( var descriptionFieldState by textFieldState(
stateValue = state.formState.description stateValue = state.formState.description
) )
@ -176,7 +178,8 @@ fun BugReportView(
AsyncImage( AsyncImage(
modifier = Modifier.fillMaxWidth(fraction = 0.5f), modifier = Modifier.fillMaxWidth(fraction = 0.5f),
model = model, model = model,
contentDescription = null contentDescription = null,
placeholder = debugPlaceholderBackground(),
) )
} }
} }
@ -209,15 +212,13 @@ fun BugReportView(
@Preview @Preview
@Composable @Composable
fun BugReportViewLightPreview() = ElementPreviewLight { ContentToPreview() } fun BugReportViewLightPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreviewLight { ContentToPreview(state) }
@Preview @Preview
@Composable @Composable
fun BugReportViewDarkPreview() = ElementPreviewDark { ContentToPreview() } fun BugReportViewDarkPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreviewDark { ContentToPreview(state) }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview(state: BugReportState) {
BugReportView( BugReportView(state = state)
state = BugReportState(),
)
} }

View file

@ -41,7 +41,6 @@ fun CrashDetectionView(
if (state.crashDetected) { if (state.crashDetected) {
CrashDetectionContent( CrashDetectionContent(
state,
onYesClicked = onOpenBugReport, onYesClicked = onOpenBugReport,
onNoClicked = ::onPopupDismissed, onNoClicked = ::onPopupDismissed,
onDismiss = ::onPopupDismissed, onDismiss = ::onPopupDismissed,
@ -51,7 +50,6 @@ fun CrashDetectionView(
@Composable @Composable
fun CrashDetectionContent( fun CrashDetectionContent(
state: CrashDetectionState,
onNoClicked: () -> Unit = { }, onNoClicked: () -> Unit = { },
onYesClicked: () -> Unit = { }, onYesClicked: () -> Unit = { },
onDismiss: () -> Unit = { }, onDismiss: () -> Unit = { },
@ -69,15 +67,15 @@ fun CrashDetectionContent(
@Preview @Preview
@Composable @Composable
fun CrashDetectionContentLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun CrashDetectionViewLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun CrashDetectionContentDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun CrashDetectionViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {
CrashDetectionContent( CrashDetectionView(
state = CrashDetectionState() state = aCrashDetectionState().copy(crashDetected = true)
) )
} }

View file

@ -17,6 +17,6 @@
package io.element.android.features.rageshake.crash.ui package io.element.android.features.rageshake.crash.ui
data class CrashDetectionState( data class CrashDetectionState(
val crashDetected: Boolean = false, val crashDetected: Boolean,
val eventSink: (CrashDetectionEvents) -> Unit = {} val eventSink: (CrashDetectionEvents) -> Unit
) )

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2022 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.rageshake.crash.ui
fun aCrashDetectionState() = CrashDetectionState(
crashDetected = false,
eventSink = {}
)

View file

@ -21,9 +21,9 @@ import io.element.android.features.rageshake.preferences.RageshakePreferencesSta
@Stable @Stable
data class RageshakeDetectionState( data class RageshakeDetectionState(
val takeScreenshot: Boolean = false, val takeScreenshot: Boolean,
val showDialog: Boolean = false, val showDialog: Boolean,
val isStarted: Boolean = false, val isStarted: Boolean,
val preferenceState: RageshakePreferencesState = RageshakePreferencesState(), val preferenceState: RageshakePreferencesState,
val eventSink: (RageshakeDetectionEvents) -> Unit = {} val eventSink: (RageshakeDetectionEvents) -> Unit
) )

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2022 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.rageshake.detection
import io.element.android.features.rageshake.preferences.aRageshakePreferencesState
fun aRageshakeDetectionState() = RageshakeDetectionState(
takeScreenshot = false,
showDialog = false,
isStarted = false,
preferenceState = aRageshakePreferencesState(),
eventSink = {}
)

View file

@ -101,11 +101,11 @@ fun RageshakeDialogContent(
@Preview @Preview
@Composable @Composable
fun RageshakeDialogContentLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun RageshakeDialogContentLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun RageshakeDialogContentDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun RageshakeDialogContentDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {

View file

@ -17,8 +17,8 @@
package io.element.android.features.rageshake.preferences package io.element.android.features.rageshake.preferences
data class RageshakePreferencesState( data class RageshakePreferencesState(
val isEnabled: Boolean = false, val isEnabled: Boolean,
val isSupported: Boolean = true, val isSupported: Boolean,
val sensitivity: Float = 0.3f, val sensitivity: Float,
val eventSink: (RageshakePreferencesEvents) -> Unit = {}, val eventSink: (RageshakePreferencesEvents) -> Unit,
) )

View file

@ -0,0 +1,34 @@
/*
* 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.rageshake.preferences
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class RageshakePreferencesStateProvider : PreviewParameterProvider<RageshakePreferencesState> {
override val values: Sequence<RageshakePreferencesState>
get() = sequenceOf(
aRageshakePreferencesState().copy(isEnabled = true, isSupported = true, sensitivity = 0.5f),
aRageshakePreferencesState().copy(isEnabled = true, isSupported = false, sensitivity = 0.5f),
)
}
fun aRageshakePreferencesState() = RageshakePreferencesState(
isEnabled = false,
isSupported = true,
sensitivity = 0.3f,
eventSink = {}
)

View file

@ -23,6 +23,7 @@ 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 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.PreferenceSlide import io.element.android.libraries.designsystem.components.preferences.PreferenceSlide
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
@ -77,26 +78,15 @@ fun RageshakePreferencesView(
@Preview @Preview
@Composable @Composable
fun RageshakePreferencesViewLightPreview() = ElementPreviewLight { ContentToPreview() } fun RageshakePreferencesViewLightPreview(@PreviewParameter(RageshakePreferencesStateProvider::class) state: RageshakePreferencesState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview @Preview
@Composable @Composable
fun RageshakePreferencesViewDarkPreview() = ElementPreviewDark { ContentToPreview() } fun RageshakePreferencesViewDarkPreview(@PreviewParameter(RageshakePreferencesStateProvider::class) state: RageshakePreferencesState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview(state: RageshakePreferencesState) {
RageshakePreferencesView(RageshakePreferencesState(isEnabled = true, isSupported = true, sensitivity = 0.5f)) RageshakePreferencesView(state)
}
@Preview
@Composable
fun RageshakePreferencesViewNotSupportedLightPreview() = ElementPreviewLight { ContentNotSupportedToPreview() }
@Preview
@Composable
fun RageshakePreferencesViewNotSupportedDarkPreview() = ElementPreviewDark { ContentNotSupportedToPreview() }
@Composable
private fun ContentNotSupportedToPreview() {
RageshakePreferencesView(RageshakePreferencesState(isEnabled = true, isSupported = false, sensitivity = 0.5f))
} }

View file

@ -107,7 +107,8 @@ class RoomListPresenter @Inject constructor(
val userDisplayName = client.loadUserDisplayName().getOrNull() val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData = val avatarData =
AvatarData( AvatarData(
name = userDisplayName ?: client.userId().value, id = client.userId().value,
name = userDisplayName,
url = userAvatarUrl, url = userAvatarUrl,
size = AvatarSize.SMALL size = AvatarSize.SMALL
) )
@ -136,6 +137,7 @@ class RoomListPresenter @Inject constructor(
is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier) is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier)
is RoomSummary.Filled -> { is RoomSummary.Filled -> {
val avatarData = AvatarData( val avatarData = AvatarData(
id = roomSummary.identifier(),
name = roomSummary.details.name, name = roomSummary.details.name,
url = roomSummary.details.avatarURLString url = roomSummary.details.avatarURLString
) )

View file

@ -32,20 +32,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
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.unit.Velocity import androidx.compose.ui.unit.Velocity
import io.element.android.features.roomlist.components.RoomListTopBar import io.element.android.features.roomlist.components.RoomListTopBar
import io.element.android.features.roomlist.components.RoomSummaryRow import io.element.android.features.roomlist.components.RoomSummaryRow
import io.element.android.features.roomlist.model.RoomListEvents import io.element.android.features.roomlist.model.RoomListEvents
import io.element.android.features.roomlist.model.RoomListRoomSummary import io.element.android.features.roomlist.model.RoomListRoomSummary
import io.element.android.features.roomlist.model.RoomListState import io.element.android.features.roomlist.model.RoomListState
import io.element.android.features.roomlist.model.stubbedRoomSummaries import io.element.android.features.roomlist.model.RoomListStateProvider
import io.element.android.libraries.designsystem.components.avatar.AvatarData
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.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.core.RoomId
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -64,7 +63,7 @@ fun RoomListView(
state.eventSink(RoomListEvents.UpdateVisibleRange(range)) state.eventSink(RoomListEvents.UpdateVisibleRange(range))
} }
RoomListView( RoomListContent(
roomSummaries = state.roomList, roomSummaries = state.roomList,
matrixUser = state.matrixUser, matrixUser = state.matrixUser,
filter = state.filter, filter = state.filter,
@ -78,7 +77,7 @@ fun RoomListView(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RoomListView( fun RoomListContent(
roomSummaries: ImmutableList<RoomListRoomSummary>, roomSummaries: ImmutableList<RoomListRoomSummary>,
matrixUser: MatrixUser?, matrixUser: MatrixUser?,
filter: String, filter: String,
@ -157,20 +156,15 @@ private fun RoomListRoomSummary.contentType() = isPlaceholder
@Preview @Preview
@Composable @Composable
fun RoomListViewLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun RoomListViewLightPreview(@PreviewParameter(RoomListStateProvider::class) state: RoomListState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview @Preview
@Composable @Composable
fun RoomListViewDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun RoomListViewDarkPreview(@PreviewParameter(RoomListStateProvider::class) state: RoomListState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview(state: RoomListState) {
RoomListView( RoomListView(state)
roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("U")),
onRoomClicked = {},
filter = "filter",
onFilterChanged = {},
onScrollOver = {}
)
} }

View file

@ -28,7 +28,9 @@ import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
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
@ -43,9 +45,13 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp 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.AvatarData
import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
@ -53,6 +59,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
import io.element.android.libraries.ui.strings.R as StringR import io.element.android.libraries.ui.strings.R as StringR
@ -170,13 +177,29 @@ fun SearchRoomListTopBar(
} }
} }
@Preview
@Composable
internal fun SearchRoomListTopBarLightPreview() = ElementPreviewLight { SearchRoomListTopBarPreview() }
@Preview
@Composable
internal fun SearchRoomListTopBarDarkPreview() = ElementPreviewDark { SearchRoomListTopBarPreview() }
@Composable
private fun SearchRoomListTopBarPreview() {
SearchRoomListTopBar(
text = "Hello",
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
)
}
@Composable @Composable
private fun DefaultRoomListTopBar( private fun DefaultRoomListTopBar(
matrixUser: MatrixUser?, matrixUser: MatrixUser?,
onOpenSettings: () -> Unit,
onSearchClicked: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior, scrollBehavior: TopAppBarScrollBehavior,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onOpenSettings: () -> Unit = {},
onSearchClicked: () -> Unit = {},
) { ) {
MediumTopAppBar( MediumTopAppBar(
modifier = modifier modifier = modifier
@ -209,3 +232,19 @@ private fun DefaultRoomListTopBar(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
} }
@Preview
@Composable
internal fun DefaultRoomListTopBarLightPreview() = ElementPreviewLight { DefaultRoomListTopBarPreview() }
@Preview
@Composable
internal fun DefaultRoomListTopBarDarkPreview() = ElementPreviewDark { DefaultRoomListTopBarPreview() }
@Composable
private fun DefaultRoomListTopBarPreview() {
DefaultRoomListTopBar(
matrixUser = MatrixUser(UserId("@id"), "Alice", AvatarData("@id", "Alice")),
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
)
}

View file

@ -46,13 +46,18 @@ import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.font.FontWeight 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.PreviewParameter
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.placeholder.material.placeholder import com.google.accompanist.placeholder.material.placeholder
import io.element.android.features.roomlist.model.RoomListRoomSummary import io.element.android.features.roomlist.model.RoomListRoomSummary
import io.element.android.features.roomlist.model.RoomListRoomSummaryProvider
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.ElementTheme
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.roomListPlaceHolder import io.element.android.libraries.designsystem.theme.roomListPlaceHolder
@ -192,3 +197,18 @@ class PercentRectangleSizeShape(private val percent: Float) : Shape {
return Outline.Generic(path) return Outline.Generic(path)
} }
} }
@Preview
@Composable
internal fun RoomSummaryRowLightPreview(@PreviewParameter(RoomListRoomSummaryProvider::class) data: RoomListRoomSummary) =
ElementPreviewLight { ContentToPreview(data) }
@Preview
@Composable
internal fun RoomSummaryRowDarkPreview(@PreviewParameter(RoomListRoomSummaryProvider::class) data: RoomListRoomSummary) =
ElementPreviewDark { ContentToPreview(data) }
@Composable
private fun ContentToPreview(data: RoomListRoomSummary) {
RoomSummaryRow(data)
}

View file

@ -28,6 +28,6 @@ data class RoomListRoomSummary(
val hasUnread: Boolean = false, val hasUnread: Boolean = false,
val timestamp: String? = null, val timestamp: String? = null,
val lastMessage: CharSequence? = null, val lastMessage: CharSequence? = null,
val avatarData: AvatarData = AvatarData(), val avatarData: AvatarData = AvatarData(id, name),
val isPlaceholder: Boolean = false, val isPlaceholder: Boolean = false,
) )

View file

@ -27,7 +27,7 @@ object RoomListRoomSummaryPlaceholders {
name = "Short name", name = "Short name",
timestamp = "hh:mm", timestamp = "hh:mm",
lastMessage = "Last message for placeholder", lastMessage = "Last message for placeholder",
avatarData = AvatarData("S") avatarData = AvatarData(id, "S")
) )
} }

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2022 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.roomlist.model
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.RoomId
open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> {
override val values: Sequence<RoomListRoomSummary>
get() = sequenceOf(
aRoomListRoomSummary(),
aRoomListRoomSummary().copy(lastMessage = null),
aRoomListRoomSummary().copy(hasUnread = true),
aRoomListRoomSummary().copy(timestamp = "88:88"),
aRoomListRoomSummary().copy(timestamp = "88:88", hasUnread = true),
aRoomListRoomSummary().copy(isPlaceholder = true),
)
}
fun aRoomListRoomSummary() = RoomListRoomSummary(
id = "!roomId",
roomId = RoomId("!roomId"),
name = "Room name",
hasUnread = false,
timestamp = null,
lastMessage = "Last message",
avatarData = AvatarData("!roomId", "Room name"),
isPlaceholder = false,
)

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2022 New Vector Ltd * Copyright (c) 2023 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,18 +16,35 @@
package io.element.android.features.roomlist.model package io.element.android.features.roomlist.model
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
internal fun stubbedRoomSummaries(): ImmutableList<RoomListRoomSummary> { open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
override val values: Sequence<RoomListState>
get() = sequenceOf(
aRoomListState(),
)
}
internal fun aRoomListState() = RoomListState(
matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("@id", "U")),
roomList = aRoomListRoomSummaryList(),
filter = "filter",
eventSink = {}
)
internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
return persistentListOf( return persistentListOf(
RoomListRoomSummary( RoomListRoomSummary(
name = "Room", name = "Room",
hasUnread = true, hasUnread = true,
timestamp = "14:18", timestamp = "14:18",
lastMessage = "A very very very very long message which suites on two lines", lastMessage = "A very very very very long message which suites on two lines",
avatarData = AvatarData("R"), avatarData = AvatarData("!id", "R"),
id = "roomId" id = "roomId"
), ),
RoomListRoomSummary( RoomListRoomSummary(
@ -35,7 +52,7 @@ internal fun stubbedRoomSummaries(): ImmutableList<RoomListRoomSummary> {
hasUnread = false, hasUnread = false,
timestamp = "14:16", timestamp = "14:16",
lastMessage = "A short message", lastMessage = "A short message",
avatarData = AvatarData("Z"), avatarData = AvatarData("!id", "Z"),
id = "roomId2" id = "roomId2"
), ),
RoomListRoomSummaryPlaceholders.create("roomId2") RoomListRoomSummaryPlaceholders.create("roomId2")

View file

@ -217,6 +217,6 @@ private val aRoomListRoomSummary = RoomListRoomSummary(
hasUnread = true, hasUnread = true,
timestamp = A_FORMATTED_DATE, timestamp = A_FORMATTED_DATE,
lastMessage = A_MESSAGE, lastMessage = A_MESSAGE,
avatarData = AvatarData(name = A_ROOM_NAME), avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME),
isPlaceholder = false, isPlaceholder = false,
) )

View file

@ -17,6 +17,7 @@
package io.element.android.features.template package io.element.android.features.template
// TODO add your ui models. Remove the eventSink if you don't have events. // TODO add your ui models. Remove the eventSink if you don't have events.
// Do not use default value, so no member get forgotten in the presenters.
data class TemplateState( data class TemplateState(
val eventSink: (TemplateEvents) -> Unit = {} val eventSink: (TemplateEvents) -> Unit
) )

View file

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

View file

@ -22,6 +22,7 @@ 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.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
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.Text import io.element.android.libraries.designsystem.theme.components.Text
@ -41,15 +42,17 @@ fun TemplateView(
@Preview @Preview
@Composable @Composable
fun TemplateViewLightPreview() = ElementPreviewLight { ContentToPreview() } fun TemplateViewLightPreview(@PreviewParameter(TemplateStateProvider::class) state: TemplateState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview @Preview
@Composable @Composable
fun TemplateViewDarkPreview() = ElementPreviewDark { ContentToPreview() } fun TemplateViewDarkPreview(@PreviewParameter(TemplateStateProvider::class) state: TemplateState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview(state: TemplateState) {
TemplateView( TemplateView(
state = TemplateState(), state = state,
) )
} }

View file

@ -35,7 +35,8 @@ test_core = "1.4.0"
coil = "2.2.2" coil = "2.2.2"
datetime = "0.4.0" datetime = "0.4.0"
serialization_json = "1.4.1" serialization_json = "1.4.1"
showkase = "1.0.0-beta14" # Warning, also hard-coded in composeDependencies()
showkase = "1.0.0-beta17"
jsoup = "1.15.3" jsoup = "1.15.3"
appyx = "1.0.3" appyx = "1.0.3"
dependencycheck = "7.4.4" dependencycheck = "7.4.4"

View file

@ -30,8 +30,12 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
@ -88,3 +92,24 @@ fun ClickableLinkText(
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
} }
@Preview
@Composable
internal fun ClickableLinkTextLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun ClickableLinkTextDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
ClickableLinkText(
text = AnnotatedString("Hello", ParagraphStyle()),
linkAnnotationTag = "",
onClick = {},
onLongClick = {},
interactionSource = MutableInteractionSource(),
)
}

View file

@ -54,11 +54,11 @@ fun LabelledCheckbox(
@Preview @Preview
@Composable @Composable
fun LabelledCheckboxLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun LabelledCheckboxLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun LabelledCheckboxDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun LabelledCheckboxDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {

View file

@ -73,11 +73,11 @@ fun ProgressDialog(
@Preview @Preview
@Composable @Composable
fun ProgressDialogLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun ProgressDialogLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun ProgressDialogDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun ProgressDialogDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {

View file

@ -29,12 +29,14 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
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.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import io.element.android.libraries.designsystem.AvatarGradientEnd import io.element.android.libraries.designsystem.AvatarGradientEnd
import io.element.android.libraries.designsystem.AvatarGradientStart import io.element.android.libraries.designsystem.AvatarGradientStart
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.debugPlaceholderAvatar
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import timber.log.Timber import timber.log.Timber
@ -68,6 +70,7 @@ private fun ImageAvatar(
}, },
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
placeholder = debugPlaceholderAvatar(),
modifier = modifier modifier = modifier
) )
} }
@ -90,7 +93,7 @@ private fun InitialsAvatar(
) { ) {
Text( Text(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
text = avatarData.name.first().uppercase(), text = avatarData.getInitial(),
fontSize = (avatarData.size.value / 2).sp, fontSize = (avatarData.size.value / 2).sp,
color = Color.White, color = Color.White,
) )
@ -99,13 +102,15 @@ private fun InitialsAvatar(
@Preview @Preview
@Composable @Composable
fun AvatarLightPreview() = ElementPreviewLight { ContentToPreview() } fun AvatarLightPreview(@PreviewParameter(AvatarDataProvider::class) avatarData: AvatarData) =
ElementPreviewLight { ContentToPreview(avatarData) }
@Preview @Preview
@Composable @Composable
fun AvatarDarkPreview() = ElementPreviewDark { ContentToPreview() } fun AvatarDarkPreview(@PreviewParameter(AvatarDataProvider::class) avatarData: AvatarData) =
ElementPreviewDark { ContentToPreview(avatarData) }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview(avatarData: AvatarData) {
Avatar(AvatarData(name = "A")) Avatar(avatarData)
} }

View file

@ -20,7 +20,13 @@ import androidx.compose.runtime.Immutable
@Immutable @Immutable
data class AvatarData( data class AvatarData(
val name: String = "", val id: String,
val name: String?,
val url: String? = null, val url: String? = null,
val size: AvatarSize = AvatarSize.MEDIUM val size: AvatarSize = AvatarSize.MEDIUM
) ) {
fun getInitial(): String {
val firstChar = name?.firstOrNull() ?: id.getOrNull(1) ?: '?'
return firstChar.uppercase()
}
}

View file

@ -0,0 +1,34 @@
/*
* 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.avatar
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class AvatarDataProvider : PreviewParameterProvider<AvatarData> {
override val values: Sequence<AvatarData>
get() = sequenceOf(
anAvatarData(),
anAvatarData().copy(name = null),
anAvatarData().copy(url = "aUrl"),
)
}
fun anAvatarData() = AvatarData(
// Let's the id not start with a 'a'.
id = "@id_of_alice:server.org",
name = "Alice",
)

View file

@ -117,11 +117,11 @@ fun ConfirmationDialog(
@Preview @Preview
@Composable @Composable
fun ConfirmationDialogLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun ConfirmationDialogLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun ConfirmationDialogDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun ConfirmationDialogDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {

View file

@ -86,11 +86,11 @@ fun ErrorDialog(
@Preview @Preview
@Composable @Composable
fun ErrorDialogLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun ErrorDialogLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun ErrorDialogDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun ErrorDialogDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {

View file

@ -60,11 +60,11 @@ fun PreferenceCategory(
@Preview @Preview
@Composable @Composable
fun PreferenceCategoryLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun PreferenceCategoryLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun PreferenceCategoryDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun PreferenceCategoryDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {

View file

@ -116,11 +116,11 @@ fun PreferenceTopAppBar(
@Preview @Preview
@Composable @Composable
fun PreferenceViewLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun PreferenceViewLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun PreferenceViewDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun PreferenceViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {

View file

@ -88,11 +88,11 @@ fun PreferenceSlide(
@Preview @Preview
@Composable @Composable
fun PreferenceSlideLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun PreferenceSlideLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun PreferenceSlideDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun PreferenceSlideDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {

View file

@ -79,11 +79,11 @@ fun PreferenceSwitch(
@Preview @Preview
@Composable @Composable
fun PreferenceSwitchLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun PreferenceSwitchLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun PreferenceSwitchDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun PreferenceSwitchDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {

View file

@ -68,11 +68,11 @@ fun PreferenceText(
@Preview @Preview
@Composable @Composable
fun PreferenceTextLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun PreferenceTextLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun PreferenceTextDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun PreferenceTextDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {

View file

@ -0,0 +1,30 @@
/*
* 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.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class ImageVectorProvider : PreviewParameterProvider<ImageVector?> {
override val values: Sequence<ImageVector?>
get() = sequenceOf(
null,
Icons.Default.BugReport,
)
}

View file

@ -22,7 +22,11 @@ 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
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.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.toEnabledColor import io.element.android.libraries.designsystem.toEnabledColor
@ -45,3 +49,18 @@ fun PreferenceIcon(
Spacer(modifier = modifier.width(56.dp)) Spacer(modifier = modifier.width(56.dp))
} }
} }
@Preview
@Composable
internal fun PreferenceIconLightPreview(@PreviewParameter(ImageVectorProvider::class) content: ImageVector?) =
ElementPreviewLight { ContentToPreview(content) }
@Preview
@Composable
internal fun PreferenceIconDarkPreview(@PreviewParameter(ImageVectorProvider::class) content: ImageVector?) =
ElementPreviewDark { ContentToPreview(content) }
@Composable
private fun ContentToPreview(content: ImageVector?) {
PreferenceIcon(content)
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.preview
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import io.element.android.libraries.designsystem.R
/**
* I wanted to set up a FakeImageLoader as per https://github.com/coil-kt/coil/issues/1327
* but it does not render in preview. In the meantime, you can use this trick to have image.
*/
@Composable
fun debugPlaceholder(
@DrawableRes debugPreview: Int,
nonDebugPainter: Painter? = null,
) = if (LocalInspectionMode.current) {
painterResource(id = debugPreview)
} else {
nonDebugPainter
}
@Composable
fun debugPlaceholderBackground(nonDebugPainter: Painter? = null): Painter? {
return debugPlaceholder(debugPreview = R.drawable.sample_background, nonDebugPainter)
}
@Composable
fun debugPlaceholderAvatar(nonDebugPainter: Painter? = null): Painter? {
return debugPlaceholder(debugPreview = R.drawable.sample_avatar, nonDebugPainter)
}

View file

@ -47,11 +47,11 @@ fun ElementColors.roomListPlaceHolder() = if (isLight) SystemGrey6Light else Sys
@Preview @Preview
@Composable @Composable
fun ColorAliasesLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun ColorAliasesLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview @Preview
@Composable @Composable
fun ColorAliasesDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun ColorAliasesDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {

View file

@ -36,7 +36,7 @@ fun elementColorsDark() = ElementColors(
// TODO Lots of colors are missing // TODO Lots of colors are missing
val materialColorSchemeDark = darkColorScheme( val materialColorSchemeDark = darkColorScheme(
primary = Color.White, primary = Color.White,
// TODO onPrimary = ColorDarkTokens.OnPrimary, onPrimary = Color.Black,
// TODO primaryContainer = ColorDarkTokens.PrimaryContainer, // TODO primaryContainer = ColorDarkTokens.PrimaryContainer,
// TODO onPrimaryContainer = ColorDarkTokens.OnPrimaryContainer, // TODO onPrimaryContainer = ColorDarkTokens.OnPrimaryContainer,
// TODO inversePrimary = ColorDarkTokens.InversePrimary, // TODO inversePrimary = ColorDarkTokens.InversePrimary,

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