Merge develop into feature/fga/update_rust_sdk
This commit is contained in:
commit
22fd4ac7f0
410 changed files with 2937 additions and 607 deletions
32
.github/ISSUE_TEMPLATE/story.yml
vendored
Normal file
32
.github/ISSUE_TEMPLATE/story.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
name: User story issue
|
||||
description: Second-level planning issue template. A story should take about a week or a sprint to finish.
|
||||
title: "[Story] <title>"
|
||||
labels: [T-Story]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: A story should take roughly a week or a sprint to finish. Each story is usually made up of a number of tasks that take half to a full day.
|
||||
value: |
|
||||
As a user…
|
||||
I want to…
|
||||
so that I can…
|
||||
|
||||
## Scope
|
||||
<!-- These should be a list of technical tasks which take ½-1 day to complete -->
|
||||
```[tasklist]
|
||||
### Tasklist
|
||||
- [ ] Task 1
|
||||
- [ ] QA signoff on completion
|
||||
- [ ] Design signoff on completion
|
||||
- [ ] Product signoff on completion
|
||||
```
|
||||
|
||||
## Stretch goals
|
||||
None at this time
|
||||
<!-- or add a tasklist -->
|
||||
|
||||
## Out of scope
|
||||
-
|
||||
validations:
|
||||
required: false
|
||||
6
.github/workflows/nightly.yml
vendored
6
.github/workflows/nightly.yml
vendored
|
|
@ -32,3 +32,9 @@ jobs:
|
|||
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
||||
ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }}
|
||||
FIREBASE_TOKEN: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_FIREBASE_TOKEN }}
|
||||
- name: Additionally upload Nightly APK to browserstack for testing
|
||||
continue-on-error: true # don't block anything by this upload failing (for now)
|
||||
run: curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_PASSWORD" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@app/build/outputs/apk/nightly/app-nightly.apk" -F "custom_id=element-x-android-nightly"
|
||||
env:
|
||||
BROWSERSTACK_USERNAME: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_USERNAME }}
|
||||
BROWSERSTACK_PASSWORD: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_ACCESS_KEY }}
|
||||
|
|
|
|||
7
.github/workflows/nightly_manual.yml
vendored
7
.github/workflows/nightly_manual.yml
vendored
|
|
@ -21,3 +21,10 @@ jobs:
|
|||
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
||||
ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }}
|
||||
FIREBASE_TOKEN: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_FIREBASE_TOKEN }}
|
||||
- name: Additionally upload Nightly APK to browserstack for testing
|
||||
continue-on-error: true # don't block anything by this upload failing (for now)
|
||||
run: curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_PASSWORD" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@app/build/outputs/apk/nightly/app-nightly.apk" -F "custom_id=element-x-android-nightly"
|
||||
env:
|
||||
BROWSERSTACK_USERNAME: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_USERNAME }}
|
||||
BROWSERSTACK_PASSWORD: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_ACCESS_KEY }}
|
||||
|
||||
|
|
|
|||
24
.github/workflows/triage-labelled.yml
vendored
24
.github/workflows/triage-labelled.yml
vendored
|
|
@ -52,3 +52,27 @@ jobs:
|
|||
env:
|
||||
PROJECT_ID: "PVT_kwDOAM0swc4ALoFY"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
verticals_feature:
|
||||
name: Add labelled issues to Verticals Feature project
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'Team: Verticals Feature')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
with:
|
||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||
query: |
|
||||
mutation add_to_project($projectid:ID!,$contentid:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
projectid: ${{ env.PROJECT_ID }}
|
||||
contentid: ${{ github.event.issue.node_id }}
|
||||
env:
|
||||
PROJECT_ID: "PVT_kwDOAM0swc4AHJKW"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ The application is a total rewrite of [Element-Android](https://github.com/vecto
|
|||
|
||||
* [Screenshots](#screenshots)
|
||||
* [Rust SDK](#rust-sdk)
|
||||
* [Roadmap](#roadmap)
|
||||
* [Contributing](#contributing)
|
||||
* [Build instructions](#build-instructions)
|
||||
* [Support](#support)
|
||||
|
|
@ -37,10 +36,6 @@ ElementX leverages the [Matrix Rust SDK](https://github.com/matrix-org/matrix-ru
|
|||
|
||||
We're doing this as a way to share code between platforms and while we've seen promising results it's still in the experimental stage and bound to change.
|
||||
|
||||
## Roadmap
|
||||
|
||||
We are aiming to have a fast and fully functional personal messaging application by the end of year 2023.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see our [contribution guide](CONTRIBUTING.md).
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.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.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
|
|
@ -31,14 +34,14 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
@Composable
|
||||
internal fun ShowkaseButton(
|
||||
isVisible: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onCloseClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
onCloseClicked: () -> Unit = {},
|
||||
) {
|
||||
if (isVisible) {
|
||||
Button(
|
||||
modifier = modifier
|
||||
.padding(top = 32.dp, start = 16.dp),
|
||||
.padding(top = 32.dp),
|
||||
onClick = onClick
|
||||
) {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,29 +19,23 @@ package io.element.android.x.root
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.detection.RageshakeDetectionPresenter
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import javax.inject.Inject
|
||||
|
||||
class RootPresenter @Inject constructor(
|
||||
private val bugReportPresenter: BugReportPresenter,
|
||||
private val crashDetectionPresenter: CrashDetectionPresenter,
|
||||
private val rageshakeDetectionPresenter: RageshakeDetectionPresenter,
|
||||
) : Presenter<RootState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): RootState {
|
||||
val isBugReportVisible = rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val isShowkaseButtonVisible = rememberSaveable {
|
||||
mutableStateOf(true)
|
||||
}
|
||||
val rageshakeDetectionState = rageshakeDetectionPresenter.present()
|
||||
val crashDetectionState = crashDetectionPresenter.present()
|
||||
val bugReportState = bugReportPresenter.present()
|
||||
|
||||
fun handleEvent(event: RootEvents) {
|
||||
when (event) {
|
||||
|
|
@ -50,11 +44,9 @@ class RootPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
return RootState(
|
||||
isBugReportVisible = isBugReportVisible.value,
|
||||
isShowkaseButtonVisible = isShowkaseButtonVisible.value,
|
||||
rageshakeDetectionState = rageshakeDetectionState,
|
||||
crashDetectionState = crashDetectionState,
|
||||
bugReportState = bugReportState,
|
||||
eventSink = ::handleEvent
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,16 +17,13 @@
|
|||
package io.element.android.x.root
|
||||
|
||||
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.detection.RageshakeDetectionState
|
||||
|
||||
@Stable
|
||||
data class RootState(
|
||||
val isBugReportVisible: Boolean,
|
||||
val isShowkaseButtonVisible: Boolean,
|
||||
val rageshakeDetectionState: RageshakeDetectionState,
|
||||
val crashDetectionState: CrashDetectionState,
|
||||
val bugReportState: BugReportState,
|
||||
val eventSink: (RootEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
@ -24,10 +24,15 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.CrashDetectionView
|
||||
import io.element.android.features.rageshake.detection.RageshakeDetectionEvents
|
||||
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.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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,12 +22,10 @@ import app.cash.molecule.RecompositionClock
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.rageshake.bugreport.BugReportPresenter
|
||||
import io.element.android.features.rageshake.crash.ui.CrashDetectionPresenter
|
||||
import io.element.android.features.rageshake.detection.RageshakeDetectionPresenter
|
||||
import io.element.android.features.rageshake.preferences.RageshakePreferencesPresenter
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -58,17 +56,11 @@ class RootPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createPresenter(): RootPresenter {
|
||||
private fun createPresenter(): RootPresenter {
|
||||
val crashDataStore = FakeCrashDataStore()
|
||||
val rageshakeDataStore = FakeRageshakeDataStore()
|
||||
val rageshake = FakeRageShake()
|
||||
val screenshotHolder = FakeScreenshotHolder()
|
||||
val bugReportPresenter = BugReportPresenter(
|
||||
bugReporter = FakeBugReporter(),
|
||||
crashDataStore = crashDataStore,
|
||||
screenshotHolder = screenshotHolder,
|
||||
appCoroutineScope = this,
|
||||
)
|
||||
val crashDetectionPresenter = CrashDetectionPresenter(
|
||||
crashDataStore = crashDataStore
|
||||
)
|
||||
|
|
@ -81,7 +73,6 @@ class RootPresenterTest {
|
|||
)
|
||||
)
|
||||
return RootPresenter(
|
||||
bugReportPresenter = bugReportPresenter,
|
||||
crashDetectionPresenter = crashDetectionPresenter,
|
||||
rageshakeDetectionPresenter = rageshakeDetectionPresenter,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -151,6 +151,12 @@ allprojects {
|
|||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
tasks.withType<Test> {
|
||||
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
apply(plugin = "kover")
|
||||
}
|
||||
|
|
@ -178,6 +184,9 @@ koverMerged {
|
|||
"*ComposableSingletons$*",
|
||||
"*_AssistedFactory_Impl*",
|
||||
"*BuildConfig",
|
||||
// Generated by Showkase
|
||||
"*Ioelementandroid*PreviewKt$*",
|
||||
"*Ioelementandroid*PreviewKt",
|
||||
// Other
|
||||
// We do not cover Nodes (normally covered by maestro, but code coverage is not computed with maestro)
|
||||
"*Node",
|
||||
|
|
@ -196,10 +205,12 @@ koverMerged {
|
|||
name = "Global minimum code coverage."
|
||||
target = kotlinx.kover.api.VerificationTarget.ALL
|
||||
bound {
|
||||
minValue = 45
|
||||
minValue = 55
|
||||
// Setting a max value, so that if coverage is bigger, it means that we have to change minValue.
|
||||
maxValue = 50
|
||||
counter = kotlinx.kover.api.CounterType.LINE
|
||||
// For instance if we have minValue = 25 and maxValue = 30, and current code coverage is now 37.32%, update
|
||||
// minValue to 35 and maxValue to 40.
|
||||
maxValue = 60
|
||||
counter = kotlinx.kover.api.CounterType.INSTRUCTION
|
||||
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
|
||||
}
|
||||
}
|
||||
|
|
@ -217,7 +228,7 @@ koverMerged {
|
|||
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 {
|
||||
name = "Check code coverage of states"
|
||||
target = kotlinx.kover.api.VerificationTarget.CLASS
|
||||
|
|
@ -230,5 +241,19 @@ koverMerged {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@
|
|||
* [Application](#application)
|
||||
* [Jetpack Compose](#jetpack-compose)
|
||||
* [Global architecture](#global-architecture)
|
||||
* [Template](#template)
|
||||
* [Template and naming](#template-and-naming)
|
||||
* [Push](#push)
|
||||
* [Dependencies management](#dependencies-management)
|
||||
* [Test](#test)
|
||||
* [Code coverage](#code-coverage)
|
||||
* [Other points](#other-points)
|
||||
* [Logging](#logging)
|
||||
* [Rageshake](#rageshake)
|
||||
|
|
@ -130,7 +131,7 @@ About Preview
|
|||
|
||||
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)
|
||||
- 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.
|
||||
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.
|
||||
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`.
|
||||
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.
|
||||
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).
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
[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
|
||||
|
||||
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).
|
||||
- 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).
|
||||
- Tests on presenter with Molecule and [Turbine](https://github.com/cashapp/turbine) (TODO this is coming)
|
||||
- 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](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
|
||||
|
||||
|
|
@ -230,6 +258,6 @@ Rageshake can be very useful to get logs from a release version of the applicati
|
|||
|
||||
The team is here to support you, feel free to ask anything to other developers.
|
||||
|
||||
Also please feel to update this documentation, if incomplete/wrong/obsolete/etc.
|
||||
Also please feel free to update this documentation, if incomplete/wrong/obsolete/etc.
|
||||
|
||||
**Thanks!**
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ package io.element.android.features.login.changeserver
|
|||
import io.element.android.libraries.architecture.Async
|
||||
|
||||
data class ChangeServerState(
|
||||
val homeserver: String = "",
|
||||
val changeServerAction: Async<Unit> = Async.Uninitialized,
|
||||
val eventSink: (ChangeServerEvents) -> Unit = {},
|
||||
val homeserver: String,
|
||||
val changeServerAction: Async<Unit>,
|
||||
val eventSink: (ChangeServerEvents) -> Unit,
|
||||
) {
|
||||
val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Async.Loading
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
@ -42,17 +42,18 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.login.R
|
||||
import io.element.android.features.login.error.changeServerError
|
||||
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.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.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.Text
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
|
|
@ -90,12 +91,13 @@ fun ChangeServerView(
|
|||
shape = RoundedCornerShape(32.dp)
|
||||
)
|
||||
) {
|
||||
VectorIcon(
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(width = 48.dp, height = 48.dp),
|
||||
// TODO Update with design input
|
||||
resourceId = R.drawable.ic_baseline_dataset_24,
|
||||
contentDescription = "",
|
||||
)
|
||||
}
|
||||
Text(
|
||||
|
|
@ -179,15 +181,15 @@ fun ChangeServerView(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ChangeServerViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun ChangeServerViewLightPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ChangeServerViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
ChangeServerView(
|
||||
state = ChangeServerState(homeserver = "matrix.org"),
|
||||
)
|
||||
private fun ContentToPreview(state: ChangeServerState) {
|
||||
ChangeServerView(state = state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,16 +221,16 @@ fun LoginRootScreen(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoginRootScreenLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun LoginRootScreenLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoginRootScreenDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun LoginRootScreenDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
LoginRootScreen(
|
||||
state = LoginRootState(
|
||||
state = aLoginRootState().copy(
|
||||
homeserver = "matrix.org",
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ import io.element.android.libraries.matrix.core.SessionId
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
data class LoginRootState(
|
||||
val homeserver: String = "",
|
||||
val loggedInState: LoggedInState = LoggedInState.NotLoggedIn,
|
||||
val formState: LoginFormState = LoginFormState.Default,
|
||||
val eventSink: (LoginRootEvents) -> Unit = {}
|
||||
val homeserver: String,
|
||||
val loggedInState: LoggedInState,
|
||||
val formState: LoginFormState,
|
||||
val eventSink: (LoginRootEvents) -> Unit
|
||||
) {
|
||||
val submitEnabled =
|
||||
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState != LoggedInState.LoggingIn
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
@ -92,13 +92,13 @@ fun LogoutPreferenceContent(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LogoutPreferenceViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun LogoutPreferenceViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LogoutPreferenceViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun LogoutPreferenceViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
LogoutPreferenceView(LogoutPreferenceState())
|
||||
LogoutPreferenceView(aLogoutPreferenceState())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,6 @@ package io.element.android.features.logout
|
|||
import io.element.android.libraries.architecture.Async
|
||||
|
||||
data class LogoutPreferenceState(
|
||||
val logoutAction: Async<Unit> = Async.Uninitialized,
|
||||
val eventSink: (LogoutPreferenceEvents) -> Unit = {},
|
||||
val logoutAction: Async<Unit>,
|
||||
val eventSink: (LogoutPreferenceEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,24 +14,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components
|
||||
package io.element.android.features.logout
|
||||
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
||||
@Composable
|
||||
fun VectorIcon(
|
||||
resourceId: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
tint: Color = LocalContentColor.current,
|
||||
) {
|
||||
androidx.compose.material3.Icon(
|
||||
painter = painterResource(id = resourceId),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
tint = tint
|
||||
)
|
||||
}
|
||||
fun aLogoutPreferenceState() = LogoutPreferenceState(
|
||||
logoutAction = Async.Uninitialized,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
@ -67,7 +67,8 @@ class MessagesPresenter @Inject constructor(
|
|||
LaunchedEffect(syncUpdateFlow) {
|
||||
roomAvatar.value =
|
||||
AvatarData(
|
||||
name = room.bestName,
|
||||
id = room.roomId.value,
|
||||
name = room.name,
|
||||
url = room.avatarUrl,
|
||||
size = AvatarSize.SMALL
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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()),
|
||||
),
|
||||
actionListState = anActionListState(),
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
@ -49,6 +49,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.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.libraries.designsystem.components.avatar.Avatar
|
||||
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.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
|
|
@ -72,7 +76,7 @@ import timber.log.Timber
|
|||
fun MessagesView(
|
||||
state: MessagesState,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit,
|
||||
onBackPressed: () -> Unit = {},
|
||||
) {
|
||||
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ data class ActionListState(
|
|||
val target: Target,
|
||||
val eventSink: (ActionListEvents) -> Unit,
|
||||
) {
|
||||
|
||||
sealed interface Target {
|
||||
object None : Target
|
||||
data class Loading(val event: TimelineItem.Event) : Target
|
||||
|
|
|
|||
|
|
@ -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.aTimelineItemEvent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
override val values: Sequence<ActionListState>
|
||||
get() = sequenceOf(
|
||||
anActionListState(),
|
||||
anActionListState().copy(target = ActionListState.Target.Loading(aTimelineItemEvent())),
|
||||
anActionListState().copy(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(),
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun anActionListState() = ActionListState(
|
||||
target = ActionListState.Target.None,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
@ -28,7 +28,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ListItem
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.ModalBottomSheetState
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.Text
|
||||
|
|
@ -38,11 +37,14 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
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 io.element.android.features.messages.actionlist.model.TimelineItemAction
|
||||
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 kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -115,13 +117,14 @@ private fun SheetContent(
|
|||
text = {
|
||||
Text(
|
||||
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 = {
|
||||
VectorIcon(
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
@ -18,6 +18,10 @@ package io.element.android.features.messages.textcomposer
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import 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.textcomposer.TextComposer
|
||||
|
||||
|
|
@ -55,3 +59,18 @@ fun MessageComposerView(
|
|||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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.TimelineItem
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemReactions
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.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(),
|
||||
highlightedEventId = null,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList<TimelineItem> {
|
||||
return persistentListOf(
|
||||
// 3 items (First Middle Last) with isMine = false
|
||||
aTimelineItemEvent(
|
||||
isMine = false,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.Last
|
||||
),
|
||||
aTimelineItemEvent(
|
||||
isMine = false,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.Middle
|
||||
),
|
||||
aTimelineItemEvent(
|
||||
isMine = false,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.First
|
||||
),
|
||||
// 3 items (First Middle Last) with isMine = true
|
||||
aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.Last
|
||||
),
|
||||
aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.Middle
|
||||
),
|
||||
aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.First
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aTimelineItemEvent(
|
||||
isMine: Boolean = false,
|
||||
content: TimelineItemEventContent = aTimelineItemContent(),
|
||||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First
|
||||
): TimelineItem.Event {
|
||||
val randomId = Math.random().toString()
|
||||
return TimelineItem.Event(
|
||||
id = randomId,
|
||||
eventId = EventId(randomId),
|
||||
senderId = "@senderId",
|
||||
senderAvatar = AvatarData("@senderId", "sender"),
|
||||
content = content,
|
||||
reactionsState = TimelineItemReactions(
|
||||
persistentListOf(
|
||||
AggregatedReaction("👍", "1")
|
||||
)
|
||||
),
|
||||
isMine = isMine,
|
||||
senderDisplayName = "sender",
|
||||
groupPosition = groupPosition,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aTimelineItemContent(): TimelineItemEventContent {
|
||||
return TimelineItemTextContent(
|
||||
body = "Text",
|
||||
htmlDocument = null
|
||||
)
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import io.element.android.features.messages.timeline.model.bubble.BubbleState
|
||||
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.TimelineItemImageView
|
||||
|
|
@ -63,11 +64,7 @@ import io.element.android.features.messages.timeline.components.TimelineItemText
|
|||
import io.element.android.features.messages.timeline.components.TimelineItemUnknownView
|
||||
import io.element.android.features.messages.timeline.components.virtual.TimelineItemDaySeparatorView
|
||||
import io.element.android.features.messages.timeline.components.virtual.TimelineLoadingMoreIndicator
|
||||
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.TimelineItemReactions
|
||||
import io.element.android.features.messages.timeline.model.event.MessagesTimelineItemContentProvider
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEncryptedContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemImageContent
|
||||
|
|
@ -76,6 +73,7 @@ import io.element.android.features.messages.timeline.model.event.TimelineItemTex
|
|||
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||
import io.element.android.features.messages.timeline.model.virtual.TimelineItemLoadingModel
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContentProvider
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
|
|
@ -84,7 +82,6 @@ import io.element.android.libraries.designsystem.theme.components.FloatingAction
|
|||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -221,10 +218,12 @@ fun TimelineItemEventRow(
|
|||
)
|
||||
}
|
||||
MessageEventBubble(
|
||||
groupPosition = event.groupPosition,
|
||||
isMine = event.isMine,
|
||||
state = BubbleState(
|
||||
groupPosition = event.groupPosition,
|
||||
isMine = event.isMine,
|
||||
isHighlighted = isHighlighted,
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
isHighlighted = isHighlighted,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = Modifier
|
||||
|
|
@ -359,78 +358,22 @@ internal fun BoxScope.TimelineScrollHelper(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoginRootScreenLightPreview(
|
||||
@PreviewParameter(MessagesTimelineItemContentProvider::class) content: TimelineItemEventContent
|
||||
fun TimelineViewLightPreview(
|
||||
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
|
||||
) = ElementPreviewLight { ContentToPreview(content) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoginRootScreenDarkPreview(
|
||||
@PreviewParameter(MessagesTimelineItemContentProvider::class) content: TimelineItemEventContent
|
||||
fun TimelineViewDarkPreview(
|
||||
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
|
||||
) = ElementPreviewDark { ContentToPreview(content) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(content: TimelineItemEventContent) {
|
||||
val timelineItems = persistentListOf(
|
||||
// 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
|
||||
),
|
||||
)
|
||||
val timelineItems = aTimelineItemList(content)
|
||||
TimelineView(
|
||||
state = TimelineState(
|
||||
state = aTimelineState().copy(
|
||||
timelineItems = timelineItems,
|
||||
highlightedEventId = null,
|
||||
eventSink = {}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMessageEvent(
|
||||
isMine: Boolean,
|
||||
content: TimelineItemEventContent,
|
||||
groupPosition: MessagesItemGroupPosition
|
||||
): TimelineItem {
|
||||
return TimelineItem.Event(
|
||||
id = Math.random().toString(),
|
||||
senderId = "senderId",
|
||||
senderAvatar = AvatarData("sender"),
|
||||
content = content,
|
||||
reactionsState = TimelineItemReactions(
|
||||
persistentListOf(
|
||||
AggregatedReaction("👍", "1")
|
||||
)
|
||||
),
|
||||
isMine = isMine,
|
||||
senderDisplayName = "sender",
|
||||
groupPosition = groupPosition,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,16 +19,27 @@ package io.element.android.features.messages.timeline.components
|
|||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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 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.bubble.BubbleState
|
||||
import io.element.android.features.messages.timeline.model.bubble.BubbleStateProvider
|
||||
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.components.Surface
|
||||
|
||||
|
|
@ -37,33 +48,31 @@ private val BUBBLE_RADIUS = 16.dp
|
|||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MessageEventBubble(
|
||||
groupPosition: MessagesItemGroupPosition,
|
||||
isMine: Boolean,
|
||||
state: BubbleState,
|
||||
interactionSource: MutableInteractionSource,
|
||||
isHighlighted: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
fun bubbleShape(): Shape {
|
||||
return when (groupPosition) {
|
||||
MessagesItemGroupPosition.First -> if (isMine) {
|
||||
return when (state.groupPosition) {
|
||||
TimelineItemGroupPosition.First -> if (state.isMine) {
|
||||
RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS)
|
||||
} else {
|
||||
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)
|
||||
} else {
|
||||
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)
|
||||
} else {
|
||||
RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS)
|
||||
}
|
||||
MessagesItemGroupPosition.None ->
|
||||
TimelineItemGroupPosition.None ->
|
||||
RoundedCornerShape(
|
||||
BUBBLE_RADIUS,
|
||||
BUBBLE_RADIUS,
|
||||
|
|
@ -74,17 +83,17 @@ fun MessageEventBubble(
|
|||
}
|
||||
|
||||
fun Modifier.offsetForItem(): Modifier {
|
||||
return if (isMine) {
|
||||
return if (state.isMine) {
|
||||
offset(y = -(12.dp))
|
||||
} else {
|
||||
offset(x = 20.dp, y = -(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundBubbleColor = if (isHighlighted) {
|
||||
val backgroundBubbleColor = if (state.isHighlighted) {
|
||||
ElementTheme.colors.messageHighlightedBackground
|
||||
} else {
|
||||
if (isMine) {
|
||||
if (state.isMine) {
|
||||
ElementTheme.colors.messageFromMeBackground
|
||||
} else {
|
||||
ElementTheme.colors.messageFromOtherBackground
|
||||
|
|
@ -107,3 +116,31 @@ fun MessageEventBubble(
|
|||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -20,7 +20,11 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemEncryptedContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import org.matrix.rustcomponents.sdk.EncryptedMessage
|
||||
|
||||
@Composable
|
||||
fun TimelineItemEncryptedView(
|
||||
|
|
@ -34,3 +38,22 @@ fun TimelineItemEncryptedView(
|
|||
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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,9 +28,15 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
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.request.ImageRequest
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.timeline.model.event.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
|
||||
fun TimelineItemImageView(
|
||||
|
|
@ -48,7 +54,7 @@ fun TimelineItemImageView(
|
|||
.aspectRatio(content.aspectRatio),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
var isLoading = rememberSaveable(content.imageMeta) { mutableStateOf(true) }
|
||||
val isLoading = rememberSaveable(content.imageMeta) { mutableStateOf(true) }
|
||||
val context = LocalContext.current
|
||||
val model = ImageRequest.Builder(context)
|
||||
.data(content.imageMeta)
|
||||
|
|
@ -57,9 +63,24 @@ fun TimelineItemImageView(
|
|||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = null,
|
||||
placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant),
|
||||
placeholder = debugPlaceholderBackground(ColorPainter(MaterialTheme.colorScheme.surfaceVariant)),
|
||||
contentScale = ContentScale.Crop,
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,11 +65,11 @@ fun TimelineItemInformativeView(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MatrixUserRowLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun TimelineItemInformativeViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MatrixUserRowDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun TimelineItemInformativeViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
|
|
|
|||
|
|
@ -16,24 +16,15 @@
|
|||
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.features.messages.timeline.model.aTimelineItemReactions
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
||||
@Composable
|
||||
fun TimelineItemReactionsView(
|
||||
|
|
@ -52,21 +43,19 @@ fun TimelineItemReactionsView(
|
|||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@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
|
||||
) {
|
||||
Text(text = reaction.key, fontSize = 12.sp)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
internal fun TimelineItemReactionsViewLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemReactionsViewDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
TimelineItemReactionsView(
|
||||
reactionsState = aTimelineItemReactions()
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
||||
@Composable
|
||||
fun TimelineItemRedactedView(
|
||||
|
|
@ -34,3 +37,18 @@ fun TimelineItemRedactedView(
|
|||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemRedactedViewLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemRedactedViewDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
TimelineItemRedactedView(TimelineItemRedactedContent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,16 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
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 io.element.android.features.messages.timeline.components.html.HtmlDocument
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemTextBasedContentProvider
|
||||
import io.element.android.libraries.designsystem.LinkColor
|
||||
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
|
||||
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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
||||
@Composable
|
||||
fun TimelineItemUnknownView(
|
||||
|
|
@ -34,3 +37,18 @@ fun TimelineItemUnknownView(
|
|||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemUnknownViewLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemUnknownViewDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
TimelineItemUnknownView(TimelineItemUnknownContent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -42,11 +42,15 @@ import androidx.compose.ui.text.font.FontStyle
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
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.sp
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import io.element.android.libraries.designsystem.LinkColor
|
||||
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.Text
|
||||
import io.element.android.libraries.matrix.permalink.PermalinkData
|
||||
|
|
@ -99,7 +103,10 @@ private fun HtmlBody(
|
|||
when (val node = nodes.next()) {
|
||||
is TextNode -> {
|
||||
if (!node.isBlank) {
|
||||
Text(text = node.text())
|
||||
Text(
|
||||
text = node.text(),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
is Element -> {
|
||||
|
|
@ -139,7 +146,7 @@ private fun HtmlBody(
|
|||
}
|
||||
|
||||
private fun Element.isInline(): Boolean {
|
||||
return when (normalName()) {
|
||||
return when (tagName().lowercase()) {
|
||||
"del" -> true
|
||||
"mx-reply" -> false
|
||||
else -> !isBlock
|
||||
|
|
@ -156,7 +163,7 @@ private fun HtmlBlock(
|
|||
) {
|
||||
val blockModifier = modifier
|
||||
.padding(top = 4.dp)
|
||||
when (element.normalName()) {
|
||||
when (element.tagName().lowercase()) {
|
||||
"p" -> HtmlParagraph(
|
||||
paragraph = element,
|
||||
modifier = blockModifier,
|
||||
|
|
@ -230,7 +237,7 @@ private fun HtmlPreformatted(
|
|||
pre: Element,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val isCode = pre.firstElementChild()?.normalName() == "code"
|
||||
val isCode = pre.firstElementChild()?.tagName()?.lowercase() == "code"
|
||||
val backgroundColor =
|
||||
if (isCode) MaterialTheme.colorScheme.codeBackground() else Color.Unspecified
|
||||
Box(
|
||||
|
|
@ -241,6 +248,7 @@ private fun HtmlPreformatted(
|
|||
Text(
|
||||
text = pre.wholeText(),
|
||||
style = TextStyle(fontFamily = FontFamily.Monospace),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -305,7 +313,7 @@ private fun HtmlHeading(
|
|||
onTextClicked: () -> Unit = {},
|
||||
onTextLongClicked: () -> Unit = {},
|
||||
) {
|
||||
val style = when (heading.normalName()) {
|
||||
val style = when (heading.tagName().lowercase()) {
|
||||
"h1" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 30.sp)
|
||||
"h2" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 26.sp)
|
||||
"h3" -> MaterialTheme.typography.headlineMedium.copy(fontSize = 22.sp)
|
||||
|
|
@ -361,7 +369,7 @@ private fun HtmlMxReply(
|
|||
}
|
||||
}
|
||||
is Element -> {
|
||||
when (blockquoteNode.normalName()) {
|
||||
when (blockquoteNode.tagName().lowercase()) {
|
||||
"br" -> {
|
||||
append('\n')
|
||||
}
|
||||
|
|
@ -483,7 +491,7 @@ private fun AnnotatedString.Builder.appendInlineChildrenElements(
|
|||
}
|
||||
|
||||
private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors: ColorScheme) {
|
||||
when (element.normalName()) {
|
||||
when (element.tagName().lowercase()) {
|
||||
"br" -> {
|
||||
append('\n')
|
||||
}
|
||||
|
|
@ -502,6 +510,7 @@ private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors
|
|||
appendInlineChildrenElements(element.childNodes(), colors)
|
||||
}
|
||||
}
|
||||
"i",
|
||||
"em" -> {
|
||||
withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
|
||||
appendInlineChildrenElements(element.childNodes(), colors)
|
||||
|
|
@ -567,3 +576,18 @@ private fun HtmlText(
|
|||
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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
package io.element.android.features.messages.timeline.factories.event
|
||||
|
||||
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.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemReactions
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
|
|
@ -58,6 +58,7 @@ class TimelineItemEventFactory @Inject constructor(
|
|||
}
|
||||
|
||||
val senderAvatarData = AvatarData(
|
||||
id = currentSender,
|
||||
name = senderDisplayName ?: currentSender,
|
||||
url = senderAvatarUrl,
|
||||
size = AvatarSize.SMALL
|
||||
|
|
@ -86,7 +87,7 @@ class TimelineItemEventFactory @Inject constructor(
|
|||
currentTimelineItem: MatrixTimelineItem.Event,
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
index: Int
|
||||
): MessagesItemGroupPosition {
|
||||
): TimelineItemGroupPosition {
|
||||
val prevTimelineItem =
|
||||
timelineItems.getOrNull(index - 1) as? MatrixTimelineItem.Event
|
||||
val nextTimelineItem =
|
||||
|
|
@ -96,10 +97,10 @@ class TimelineItemEventFactory @Inject constructor(
|
|||
val nextSender = nextTimelineItem?.event?.sender()
|
||||
|
||||
return when {
|
||||
previousSender != currentSender && nextSender == currentSender -> MessagesItemGroupPosition.First
|
||||
previousSender == currentSender && nextSender == currentSender -> MessagesItemGroupPosition.Middle
|
||||
previousSender == currentSender && nextSender != currentSender -> MessagesItemGroupPosition.Last
|
||||
else -> MessagesItemGroupPosition.None
|
||||
previousSender != currentSender && nextSender == currentSender -> TimelineItemGroupPosition.First
|
||||
previousSender == currentSender && nextSender == currentSender -> TimelineItemGroupPosition.Middle
|
||||
previousSender == currentSender && nextSender != currentSender -> TimelineItemGroupPosition.Last
|
||||
else -> TimelineItemGroupPosition.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -46,7 +46,7 @@ sealed interface TimelineItem {
|
|||
val content: TimelineItemEventContent,
|
||||
val sentTime: String = "",
|
||||
val isMine: Boolean = false,
|
||||
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None,
|
||||
val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||
val reactionsState: TimelineItemReactions
|
||||
) : TimelineItem {
|
||||
|
||||
|
|
|
|||
|
|
@ -17,14 +17,13 @@
|
|||
package io.element.android.features.messages.timeline.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
@Immutable
|
||||
sealed interface MessagesItemGroupPosition {
|
||||
object First : MessagesItemGroupPosition
|
||||
object Middle : MessagesItemGroupPosition
|
||||
object Last : MessagesItemGroupPosition
|
||||
object None : MessagesItemGroupPosition
|
||||
sealed interface TimelineItemGroupPosition {
|
||||
object First : TimelineItemGroupPosition
|
||||
object Middle : TimelineItemGroupPosition
|
||||
object Last : TimelineItemGroupPosition
|
||||
object None : TimelineItemGroupPosition
|
||||
|
||||
fun isNew(): Boolean = when (this) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -21,9 +21,3 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
data class TimelineItemReactions(
|
||||
val reactions: ImmutableList<AggregatedReaction>
|
||||
)
|
||||
|
||||
data class AggregatedReaction(
|
||||
val key: String,
|
||||
val count: String,
|
||||
val isHighlighted: Boolean = false
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.bubble
|
||||
|
||||
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
|
||||
|
||||
data class BubbleState(
|
||||
val groupPosition: TimelineItemGroupPosition,
|
||||
val isMine: Boolean,
|
||||
val isHighlighted: Boolean,
|
||||
)
|
||||
|
|
@ -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.model.bubble
|
||||
|
||||
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,
|
||||
)
|
||||
|
|
@ -17,31 +17,6 @@
|
|||
package io.element.android.features.messages.timeline.model.event
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import org.matrix.rustcomponents.sdk.EncryptedMessage
|
||||
|
||||
@Immutable
|
||||
sealed interface TimelineItemEventContent
|
||||
|
||||
class MessagesTimelineItemContentProvider : PreviewParameterProvider<TimelineItemEventContent> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.event
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import org.jsoup.Jsoup
|
||||
import org.matrix.rustcomponents.sdk.EncryptedMessage
|
||||
|
||||
class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEventContent> {
|
||||
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
|
||||
|
|
@ -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.event
|
||||
|
||||
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,
|
||||
)
|
||||
|
|
@ -170,7 +170,7 @@ private fun aMessageEvent(
|
|||
id = AN_EVENT_ID,
|
||||
senderId = A_USER_ID.value,
|
||||
senderDisplayName = A_USER_NAME,
|
||||
senderAvatar = AvatarData(),
|
||||
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
|
||||
content = content,
|
||||
sentTime = "",
|
||||
isMine = isMine,
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ private fun aMessageEvent(
|
|||
id = AN_EVENT_ID,
|
||||
senderId = A_USER_ID.value,
|
||||
senderDisplayName = A_USER_NAME,
|
||||
senderAvatar = AvatarData(),
|
||||
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
|
||||
content = content,
|
||||
sentTime = "",
|
||||
isMine = isMine,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.text.font.FontWeight
|
||||
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.sp
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.HorizontalPagerIndicator
|
||||
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.Text
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
|
|
@ -150,6 +154,7 @@ fun OnBoardingPage(
|
|||
.padding(8.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = 24.sp,
|
||||
)
|
||||
Text(
|
||||
|
|
@ -158,7 +163,23 @@ fun OnBoardingPage(
|
|||
.fillMaxWidth()
|
||||
.align(CenterHorizontally),
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -20,15 +20,16 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.preferences.user.UserPreferences
|
||||
import io.element.android.features.rageshake.preferences.RageshakePreferencesState
|
||||
import io.element.android.features.rageshake.preferences.RageshakePreferencesView
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserProvider
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
|
|
@ -58,18 +59,15 @@ fun PreferencesRootView(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreferencesRootViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
fun PreferencesRootViewLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
|
||||
ElementPreviewLight { ContentToPreview(matrixUser) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreferencesRootViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
|
||||
ElementPreviewDark { ContentToPreview(matrixUser) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
val state = PreferencesRootState(
|
||||
logoutState = LogoutPreferenceState(),
|
||||
rageshakeState = RageshakePreferencesState(),
|
||||
myUser = Async.Uninitialized
|
||||
)
|
||||
PreferencesRootView(state)
|
||||
private fun ContentToPreview(matrixUser: MatrixUser) {
|
||||
PreferencesRootView(aPreferencesRootState().copy(myUser = Async.Success(matrixUser)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,14 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserHeader
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserWithNullProvider
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
|
||||
@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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ import io.element.android.libraries.architecture.Async
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
data class BugReportState(
|
||||
val formState: BugReportFormState = BugReportFormState.Default,
|
||||
val hasCrashLogs: Boolean = false,
|
||||
val screenshotUri: String? = null,
|
||||
val sendingProgress: Float = 0F,
|
||||
val sending: Async<Unit> = Async.Uninitialized,
|
||||
val eventSink: (BugReportEvents) -> Unit = {}
|
||||
val formState: BugReportFormState,
|
||||
val hasCrashLogs: Boolean,
|
||||
val screenshotUri: String?,
|
||||
val sendingProgress: Float,
|
||||
val sending: Async<Unit>,
|
||||
val eventSink: (BugReportEvents) -> Unit
|
||||
) {
|
||||
val submitEnabled =
|
||||
formState.description.length > 10 && sending !is Async.Loading
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
@ -40,6 +40,7 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
|
|
@ -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.preview.ElementPreviewDark
|
||||
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.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
|
||||
|
|
@ -107,7 +109,7 @@ fun BugReportView(
|
|||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
)
|
||||
var descriptionFieldState by textFieldState(
|
||||
stateValue = state.formState.description
|
||||
)
|
||||
|
|
@ -176,7 +178,8 @@ fun BugReportView(
|
|||
AsyncImage(
|
||||
modifier = Modifier.fillMaxWidth(fraction = 0.5f),
|
||||
model = model,
|
||||
contentDescription = null
|
||||
contentDescription = null,
|
||||
placeholder = debugPlaceholderBackground(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -209,15 +212,13 @@ fun BugReportView(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun BugReportViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
fun BugReportViewLightPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun BugReportViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
fun BugReportViewDarkPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
BugReportView(
|
||||
state = BugReportState(),
|
||||
)
|
||||
private fun ContentToPreview(state: BugReportState) {
|
||||
BugReportView(state = state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ fun CrashDetectionView(
|
|||
|
||||
if (state.crashDetected) {
|
||||
CrashDetectionContent(
|
||||
state,
|
||||
onYesClicked = onOpenBugReport,
|
||||
onNoClicked = ::onPopupDismissed,
|
||||
onDismiss = ::onPopupDismissed,
|
||||
|
|
@ -51,7 +50,6 @@ fun CrashDetectionView(
|
|||
|
||||
@Composable
|
||||
fun CrashDetectionContent(
|
||||
state: CrashDetectionState,
|
||||
onNoClicked: () -> Unit = { },
|
||||
onYesClicked: () -> Unit = { },
|
||||
onDismiss: () -> Unit = { },
|
||||
|
|
@ -69,15 +67,15 @@ fun CrashDetectionContent(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CrashDetectionContentLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun CrashDetectionViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CrashDetectionContentDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun CrashDetectionViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
CrashDetectionContent(
|
||||
state = CrashDetectionState()
|
||||
CrashDetectionView(
|
||||
state = aCrashDetectionState().copy(crashDetected = true)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,6 @@
|
|||
package io.element.android.features.rageshake.crash.ui
|
||||
|
||||
data class CrashDetectionState(
|
||||
val crashDetected: Boolean = false,
|
||||
val eventSink: (CrashDetectionEvents) -> Unit = {}
|
||||
val crashDetected: Boolean,
|
||||
val eventSink: (CrashDetectionEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
@ -21,9 +21,9 @@ import io.element.android.features.rageshake.preferences.RageshakePreferencesSta
|
|||
|
||||
@Stable
|
||||
data class RageshakeDetectionState(
|
||||
val takeScreenshot: Boolean = false,
|
||||
val showDialog: Boolean = false,
|
||||
val isStarted: Boolean = false,
|
||||
val preferenceState: RageshakePreferencesState = RageshakePreferencesState(),
|
||||
val eventSink: (RageshakeDetectionEvents) -> Unit = {}
|
||||
val takeScreenshot: Boolean,
|
||||
val showDialog: Boolean,
|
||||
val isStarted: Boolean,
|
||||
val preferenceState: RageshakePreferencesState,
|
||||
val eventSink: (RageshakeDetectionEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
@ -101,11 +101,11 @@ fun RageshakeDialogContent(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RageshakeDialogContentLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun RageshakeDialogContentLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RageshakeDialogContentDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun RageshakeDialogContentDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
package io.element.android.features.rageshake.preferences
|
||||
|
||||
data class RageshakePreferencesState(
|
||||
val isEnabled: Boolean = false,
|
||||
val isSupported: Boolean = true,
|
||||
val sensitivity: Float = 0.3f,
|
||||
val eventSink: (RageshakePreferencesEvents) -> Unit = {},
|
||||
val isEnabled: Boolean,
|
||||
val isSupported: Boolean,
|
||||
val sensitivity: Float,
|
||||
val eventSink: (RageshakePreferencesEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceSlide
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
|
||||
|
|
@ -77,26 +78,15 @@ fun RageshakePreferencesView(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RageshakePreferencesViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
fun RageshakePreferencesViewLightPreview(@PreviewParameter(RageshakePreferencesStateProvider::class) state: RageshakePreferencesState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RageshakePreferencesViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
fun RageshakePreferencesViewDarkPreview(@PreviewParameter(RageshakePreferencesStateProvider::class) state: RageshakePreferencesState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
RageshakePreferencesView(RageshakePreferencesState(isEnabled = true, isSupported = true, sensitivity = 0.5f))
|
||||
}
|
||||
|
||||
@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))
|
||||
private fun ContentToPreview(state: RageshakePreferencesState) {
|
||||
RageshakePreferencesView(state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,8 @@ class RoomListPresenter @Inject constructor(
|
|||
val userDisplayName = client.loadUserDisplayName().getOrNull()
|
||||
val avatarData =
|
||||
AvatarData(
|
||||
name = userDisplayName ?: client.userId().value,
|
||||
id = client.userId().value,
|
||||
name = userDisplayName,
|
||||
url = userAvatarUrl,
|
||||
size = AvatarSize.SMALL
|
||||
)
|
||||
|
|
@ -136,6 +137,7 @@ class RoomListPresenter @Inject constructor(
|
|||
is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier)
|
||||
is RoomSummary.Filled -> {
|
||||
val avatarData = AvatarData(
|
||||
id = roomSummary.identifier(),
|
||||
name = roomSummary.details.name,
|
||||
url = roomSummary.details.avatarURLString
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,20 +32,19 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import io.element.android.features.roomlist.components.RoomListTopBar
|
||||
import io.element.android.features.roomlist.components.RoomSummaryRow
|
||||
import io.element.android.features.roomlist.model.RoomListEvents
|
||||
import io.element.android.features.roomlist.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.model.RoomListState
|
||||
import io.element.android.features.roomlist.model.stubbedRoomSummaries
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.features.roomlist.model.RoomListStateProvider
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -64,7 +63,7 @@ fun RoomListView(
|
|||
state.eventSink(RoomListEvents.UpdateVisibleRange(range))
|
||||
}
|
||||
|
||||
RoomListView(
|
||||
RoomListContent(
|
||||
roomSummaries = state.roomList,
|
||||
matrixUser = state.matrixUser,
|
||||
filter = state.filter,
|
||||
|
|
@ -78,7 +77,7 @@ fun RoomListView(
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomListView(
|
||||
fun RoomListContent(
|
||||
roomSummaries: ImmutableList<RoomListRoomSummary>,
|
||||
matrixUser: MatrixUser?,
|
||||
filter: String,
|
||||
|
|
@ -157,20 +156,15 @@ private fun RoomListRoomSummary.contentType() = isPlaceholder
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RoomListViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun RoomListViewLightPreview(@PreviewParameter(RoomListStateProvider::class) state: RoomListState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RoomListViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun RoomListViewDarkPreview(@PreviewParameter(RoomListStateProvider::class) state: RoomListState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
RoomListView(
|
||||
roomSummaries = stubbedRoomSummaries(),
|
||||
matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("U")),
|
||||
onRoomClicked = {},
|
||||
filter = "filter",
|
||||
onFilterChanged = {},
|
||||
onScrollOver = {}
|
||||
)
|
||||
private fun ContentToPreview(state: RoomListState) {
|
||||
RoomListView(state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ import androidx.compose.material.icons.filled.Search
|
|||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.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.IconButton
|
||||
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.TopAppBar
|
||||
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.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
|
||||
private fun DefaultRoomListTopBar(
|
||||
matrixUser: MatrixUser?,
|
||||
onOpenSettings: () -> Unit,
|
||||
onSearchClicked: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
modifier: Modifier = Modifier,
|
||||
onOpenSettings: () -> Unit = {},
|
||||
onSearchClicked: () -> Unit = {},
|
||||
) {
|
||||
MediumTopAppBar(
|
||||
modifier = modifier
|
||||
|
|
@ -209,3 +232,19 @@ private fun DefaultRoomListTopBar(
|
|||
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()),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,13 +46,18 @@ import androidx.compose.ui.graphics.Path
|
|||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.placeholder.material.placeholder
|
||||
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.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.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.roomListPlaceHolder
|
||||
|
|
@ -192,3 +197,18 @@ class PercentRectangleSizeShape(private val percent: Float) : Shape {
|
|||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,6 @@ data class RoomListRoomSummary(
|
|||
val hasUnread: Boolean = false,
|
||||
val timestamp: String? = null,
|
||||
val lastMessage: CharSequence? = null,
|
||||
val avatarData: AvatarData = AvatarData(),
|
||||
val avatarData: AvatarData = AvatarData(id, name),
|
||||
val isPlaceholder: Boolean = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ object RoomListRoomSummaryPlaceholders {
|
|||
name = "Short name",
|
||||
timestamp = "hh:mm",
|
||||
lastMessage = "Last message for placeholder",
|
||||
avatarData = AvatarData("S")
|
||||
avatarData = AvatarData(id, "S")
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -16,18 +16,35 @@
|
|||
|
||||
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.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
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(
|
||||
RoomListRoomSummary(
|
||||
name = "Room",
|
||||
hasUnread = true,
|
||||
timestamp = "14:18",
|
||||
lastMessage = "A very very very very long message which suites on two lines",
|
||||
avatarData = AvatarData("R"),
|
||||
avatarData = AvatarData("!id", "R"),
|
||||
id = "roomId"
|
||||
),
|
||||
RoomListRoomSummary(
|
||||
|
|
@ -35,7 +52,7 @@ internal fun stubbedRoomSummaries(): ImmutableList<RoomListRoomSummary> {
|
|||
hasUnread = false,
|
||||
timestamp = "14:16",
|
||||
lastMessage = "A short message",
|
||||
avatarData = AvatarData("Z"),
|
||||
avatarData = AvatarData("!id", "Z"),
|
||||
id = "roomId2"
|
||||
),
|
||||
RoomListRoomSummaryPlaceholders.create("roomId2")
|
||||
|
|
@ -217,6 +217,6 @@ private val aRoomListRoomSummary = RoomListRoomSummary(
|
|||
hasUnread = true,
|
||||
timestamp = A_FORMATTED_DATE,
|
||||
lastMessage = A_MESSAGE,
|
||||
avatarData = AvatarData(name = A_ROOM_NAME),
|
||||
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME),
|
||||
isPlaceholder = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.template
|
||||
|
||||
// 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(
|
||||
val eventSink: (TemplateEvents) -> Unit = {}
|
||||
val eventSink: (TemplateEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
@ -22,6 +22,7 @@ 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 io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
|
@ -41,15 +42,17 @@ fun TemplateView(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun TemplateViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
fun TemplateViewLightPreview(@PreviewParameter(TemplateStateProvider::class) state: TemplateState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun TemplateViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
fun TemplateViewDarkPreview(@PreviewParameter(TemplateStateProvider::class) state: TemplateState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
private fun ContentToPreview(state: TemplateState) {
|
||||
TemplateView(
|
||||
state = TemplateState(),
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ test_core = "1.4.0"
|
|||
coil = "2.2.2"
|
||||
datetime = "0.4.0"
|
||||
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"
|
||||
appyx = "1.0.3"
|
||||
dependencycheck = "7.4.4"
|
||||
|
|
|
|||
|
|
@ -30,8 +30,12 @@ import androidx.compose.ui.geometry.Offset
|
|||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.ParagraphStyle
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
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 kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
|
|
@ -88,3 +92,24 @@ fun ClickableLinkText(
|
|||
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(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,11 +54,11 @@ fun LabelledCheckbox(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LabelledCheckboxLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun LabelledCheckboxLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LabelledCheckboxDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun LabelledCheckboxDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
|
|
|
|||
|
|
@ -73,11 +73,11 @@ fun ProgressDialog(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ProgressDialogLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun ProgressDialogLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ProgressDialogDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun ProgressDialogDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
|
|
|
|||
|
|
@ -29,12 +29,14 @@ import androidx.compose.ui.graphics.Brush
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import io.element.android.libraries.designsystem.AvatarGradientEnd
|
||||
import io.element.android.libraries.designsystem.AvatarGradientStart
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
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 timber.log.Timber
|
||||
|
||||
|
|
@ -68,6 +70,7 @@ private fun ImageAvatar(
|
|||
},
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
placeholder = debugPlaceholderAvatar(),
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
@ -90,7 +93,7 @@ private fun InitialsAvatar(
|
|||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = avatarData.name.first().uppercase(),
|
||||
text = avatarData.getInitial(),
|
||||
fontSize = (avatarData.size.value / 2).sp,
|
||||
color = Color.White,
|
||||
)
|
||||
|
|
@ -99,13 +102,15 @@ private fun InitialsAvatar(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AvatarLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
fun AvatarLightPreview(@PreviewParameter(AvatarDataProvider::class) avatarData: AvatarData) =
|
||||
ElementPreviewLight { ContentToPreview(avatarData) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AvatarDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
fun AvatarDarkPreview(@PreviewParameter(AvatarDataProvider::class) avatarData: AvatarData) =
|
||||
ElementPreviewDark { ContentToPreview(avatarData) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Avatar(AvatarData(name = "A"))
|
||||
private fun ContentToPreview(avatarData: AvatarData) {
|
||||
Avatar(avatarData)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,13 @@ import androidx.compose.runtime.Immutable
|
|||
|
||||
@Immutable
|
||||
data class AvatarData(
|
||||
val name: String = "",
|
||||
val id: String,
|
||||
val name: String?,
|
||||
val url: String? = null,
|
||||
val size: AvatarSize = AvatarSize.MEDIUM
|
||||
)
|
||||
) {
|
||||
fun getInitial(): String {
|
||||
val firstChar = name?.firstOrNull() ?: id.getOrNull(1) ?: '?'
|
||||
return firstChar.uppercase()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
@ -117,11 +117,11 @@ fun ConfirmationDialog(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ConfirmationDialogLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun ConfirmationDialogLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ConfirmationDialogDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun ConfirmationDialogDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
|
|
|
|||
|
|
@ -86,11 +86,11 @@ fun ErrorDialog(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ErrorDialogLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun ErrorDialogLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ErrorDialogDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun ErrorDialogDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
|
|
|
|||
|
|
@ -60,11 +60,11 @@ fun PreferenceCategory(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreferenceCategoryLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun PreferenceCategoryLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreferenceCategoryDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun PreferenceCategoryDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
|
|
|
|||
|
|
@ -116,11 +116,11 @@ fun PreferenceTopAppBar(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreferenceViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun PreferenceViewLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreferenceViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun PreferenceViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
|
|
|
|||
|
|
@ -88,11 +88,11 @@ fun PreferenceSlide(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreferenceSlideLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun PreferenceSlideLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreferenceSlideDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun PreferenceSlideDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
|
|
|
|||
|
|
@ -79,11 +79,11 @@ fun PreferenceSwitch(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreferenceSwitchLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun PreferenceSwitchLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreferenceSwitchDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun PreferenceSwitchDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
|
|
|
|||
|
|
@ -68,11 +68,11 @@ fun PreferenceText(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreferenceTextLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
internal fun PreferenceTextLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreferenceTextDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
internal fun PreferenceTextDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue