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

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

View file

@ -22,7 +22,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.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)
}

View file

@ -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
)
}

View file

@ -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
)

View file

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

View file

@ -24,10 +24,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.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")
}
}

View file

@ -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,
)

View file

@ -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
}
}
}
}

View file

@ -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

View file

@ -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
}

View file

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

View file

@ -42,17 +42,18 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.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)
}

View file

@ -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",
),
)

View file

@ -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

View file

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

View file

@ -92,13 +92,13 @@ fun LogoutPreferenceContent(
@Preview
@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())
}

View file

@ -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,
)

View file

@ -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 = {}
)

View file

@ -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
)

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.actionlist.anActionListState
import io.element.android.features.messages.textcomposer.aMessageComposerState
import io.element.android.features.messages.timeline.aTimelineItemContent
import io.element.android.features.messages.timeline.aTimelineItemList
import io.element.android.features.messages.timeline.aTimelineState
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.RoomId
import io.element.android.libraries.textcomposer.MessageComposerMode
open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
override val values: Sequence<MessagesState>
get() = sequenceOf(
aMessagesState(),
)
}
fun aMessagesState() = MessagesState(
roomId = RoomId("!id"),
roomName = "Room name",
roomAvatar = AvatarData("!id", "Room name"),
composerState = aMessageComposerState().copy(
text = StableCharSequence("Hello"),
isFullScreen = false,
mode = MessageComposerMode.Normal("Hello"),
),
timelineState = aTimelineState().copy(
timelineItems = aTimelineItemList(aTimelineItemContent()),
hasMoreToLoad = false,
),
actionListState = anActionListState(),
eventSink = {}
)

View file

@ -49,6 +49,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.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)
}

View file

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

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.actionlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.features.messages.timeline.aMessageEvent
import kotlinx.collections.immutable.persistentListOf
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
override val values: Sequence<ActionListState>
get() = sequenceOf(
anActionListState(),
anActionListState().copy(target = ActionListState.Target.Loading(aMessageEvent())),
anActionListState().copy(
target = ActionListState.Target.Success(
messageEvent = aMessageEvent(),
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Copy,
TimelineItemAction.Edit,
TimelineItemAction.Redact,
)
)
)
)
}
fun anActionListState() = ActionListState(
target = ActionListState.Target.None,
eventSink = {}
)

View file

@ -28,7 +28,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.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)
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.textcomposer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.textcomposer.MessageComposerMode
open class MessageComposerStateProvider : PreviewParameterProvider<MessageComposerState> {
override val values: Sequence<MessageComposerState>
get() = sequenceOf(
aMessageComposerState(),
)
}
fun aMessageComposerState() = MessageComposerState(
text = StableCharSequence(""),
isFullScreen = false,
mode = MessageComposerMode.Normal(content = ""),
eventSink = {}
)

View file

@ -18,6 +18,10 @@ package io.element.android.features.messages.textcomposer
import androidx.compose.runtime.Composable
import androidx.compose.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)
}

View file

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

View file

@ -0,0 +1,100 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.features.messages.timeline.model.content.TimelineItemContent
import io.element.android.features.messages.timeline.model.content.TimelineItemTextContent
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.EventId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
fun aTimelineState() = TimelineState(
timelineItems = persistentListOf(),
hasMoreToLoad = false,
highlightedEventId = null,
eventSink = {}
)
internal fun aTimelineItemList(content: TimelineItemContent): ImmutableList<TimelineItem> {
return persistentListOf(
// 3 items (First Middle Last) with isMine = false
aMessageEvent(
isMine = false,
content = content,
groupPosition = TimelineItemGroupPosition.Last
),
aMessageEvent(
isMine = false,
content = content,
groupPosition = TimelineItemGroupPosition.Middle
),
aMessageEvent(
isMine = false,
content = content,
groupPosition = TimelineItemGroupPosition.First
),
// 3 items (First Middle Last) with isMine = true
aMessageEvent(
isMine = true,
content = content,
groupPosition = TimelineItemGroupPosition.Last
),
aMessageEvent(
isMine = true,
content = content,
groupPosition = TimelineItemGroupPosition.Middle
),
aMessageEvent(
isMine = true,
content = content,
groupPosition = TimelineItemGroupPosition.First
),
)
}
internal fun aMessageEvent(
isMine: Boolean = false,
content: TimelineItemContent = aTimelineItemContent(),
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First
): TimelineItem.MessageEvent {
return TimelineItem.MessageEvent(
id = EventId(Math.random().toString()),
senderId = "@senderId",
senderAvatar = AvatarData("@senderId", "sender"),
content = content,
reactionsState = TimelineItemReactions(
persistentListOf(
AggregatedReaction("👍", "1")
)
),
isMine = isMine,
senderDisplayName = "sender",
groupPosition = groupPosition,
)
}
internal fun aTimelineItemContent(): TimelineItemContent {
return TimelineItemTextContent(
body = "Text",
htmlDocument = null
)
}

View file

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

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.components
import androidx.compose.runtime.Stable
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
@Stable
data class BubbleState(
val groupPosition: TimelineItemGroupPosition,
val isMine: Boolean,
val isHighlighted: Boolean,
)

View file

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

View file

@ -19,16 +19,25 @@ package io.element.android.features.messages.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.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.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 +46,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 +81,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 +114,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))
}
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.AggregatedReactionProvider
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.surfaceVariant,
border = BorderStroke(2.dp, MaterialTheme.colorScheme.background),
shape = RoundedCornerShape(corner = CornerSize(12.dp)),
) {
Row(
modifier = Modifier.padding(vertical = 5.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// TODO `reaction.isHighlighted` is not used.
Text(text = reaction.key, fontSize = 12.sp)
Spacer(modifier = Modifier.width(4.dp))
Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp)
}
}
}
@Preview
@Composable
internal fun MessagesReactionButtonLightPreview(@PreviewParameter(AggregatedReactionProvider::class) reaction: AggregatedReaction) =
ElementPreviewLight { ContentToPreview(reaction) }
@Preview
@Composable
internal fun MessagesReactionButtonDarkPreview(@PreviewParameter(AggregatedReactionProvider::class) reaction: AggregatedReaction) =
ElementPreviewDark { ContentToPreview(reaction) }
@Composable
private fun ContentToPreview(reaction: AggregatedReaction) {
MessagesReactionButton(reaction)
}

View file

@ -20,7 +20,11 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.features.messages.timeline.model.content.TimelineItemEncryptedContent
import io.element.android.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,
)
)
}

View file

@ -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.content.TimelineItemImageContent
import io.element.android.features.messages.timeline.model.content.TimelineItemImageContentProvider
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
@Composable
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)
}

View file

@ -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() {

View file

@ -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()
)
}

View file

@ -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.content.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)
}

View file

@ -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.content.TimelineItemTextBasedContent
import io.element.android.features.messages.timeline.model.content.TimelineItemTextBasedContentProvider
import io.element.android.libraries.designsystem.LinkColor
import io.element.android.libraries.designsystem.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())
}

View file

@ -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.content.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)
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.components.html
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
open class DocumentProvider : PreviewParameterProvider<Document> {
override val values: Sequence<Document>
get() = sequenceOf(
"text",
"<strong>Strong</strong>",
"<b>Bold</b>",
"<i>Italic</i>",
// FIXME This does not work
"<b><i>Bold then italic</i></b>",
// FIXME This does not work
"<i><b>Italic then bold</b></i>",
"<em>em</em>",
"<unknown>unknown</unknown>",
// FIXME `br` is not rendered correctly in the Preview.
"Line 1<br/>Line 2",
"<code>code</code>",
"<del>del</del>",
"<h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3><h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6><h7>Heading 7</h7>",
"<a href=\"https://matrix.org\">link</a>",
"<p>paragraph</p>",
"<p>paragraph 1</p><p>paragraph 2</p>",
"<ol><li>ol item 1</li><li>ol item 2</li></ol>",
"<ol><li><i>ol item 1 italic</i></li><li><b>ol item 2 bold</b></li></ol>",
"<ul><li>ul item 1</li><li>ul item 2</li></ul>",
"<blockquote>blockquote</blockquote>",
// TODO Find a way to make is work with `pre`. For now there is an error with
// jsoup: java.lang.NoSuchMethodError: 'org.jsoup.nodes.Element org.jsoup.nodes.Element.firstElementChild()'
// "<pre>pre</pre>",
"<mx-reply><blockquote><a href=\\\"https://matrix.to/#/!roomId/\$eventId?via=matrix.org\\\">In reply to</a> " +
"<a href=\\\"https://matrix.to/#/@alice:matrix.org\\\">@alice:matrix.org</a><br>original message</blockquote></mx-reply>reply",
).map { Jsoup.parse(it) }
}

View file

@ -42,11 +42,15 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.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())
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.model
data class AggregatedReaction(
val key: String,
val count: String,
val isHighlighted: Boolean = false
)

View file

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

View file

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

View file

@ -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,
)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.model.content
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import org.jsoup.Jsoup
import org.matrix.rustcomponents.sdk.EncryptedMessage
class TimelineItemContentProvider : PreviewParameterProvider<TimelineItemContent> {
override val values = sequenceOf(
aTimelineItemEmoteContent(),
aTimelineItemEncryptedContent(),
// TODO MessagesTimelineItemImageContent(),
aTimelineItemNoticeContent(),
aTimelineItemRedactedContent(),
aTimelineItemTextContent(),
aTimelineItemUnknownContent(),
)
}
class TimelineItemTextBasedContentProvider : PreviewParameterProvider<TimelineItemTextBasedContent> {
override val values = sequenceOf(
aTimelineItemEmoteContent(),
aTimelineItemEmoteContent().copy(htmlDocument = Jsoup.parse("Emote")),
aTimelineItemNoticeContent(),
aTimelineItemNoticeContent().copy(htmlDocument = Jsoup.parse("Notice")),
aTimelineItemTextContent(),
aTimelineItemTextContent().copy(htmlDocument = Jsoup.parse("Text")),
)
}
fun aTimelineItemEmoteContent() = TimelineItemEmoteContent(
body = "Emote",
htmlDocument = null
)
fun aTimelineItemEncryptedContent() = TimelineItemEncryptedContent(
encryptedMessage = EncryptedMessage.Unknown
)
fun aTimelineItemNoticeContent() = TimelineItemNoticeContent(
body = "Notice",
htmlDocument = null
)
fun aTimelineItemRedactedContent() = TimelineItemRedactedContent
fun aTimelineItemTextContent() = TimelineItemTextContent(
body = "Text",
htmlDocument = null
)
fun aTimelineItemUnknownContent() = TimelineItemUnknownContent

View file

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

View file

@ -170,7 +170,7 @@ private fun aMessageEvent(
id = AN_EVENT_ID,
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,

View file

@ -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,

View file

@ -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()
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.root
import io.element.android.features.logout.aLogoutPreferenceState
import io.element.android.features.rageshake.preferences.aRageshakePreferencesState
import io.element.android.libraries.architecture.Async
fun aPreferencesRootState() = PreferencesRootState(
logoutState = aLogoutPreferenceState(),
rageshakeState = aRageshakePreferencesState(),
myUser = Async.Uninitialized
)

View file

@ -20,15 +20,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.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)))
}

View file

@ -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))
}
}

View file

@ -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

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.rageshake.bugreport
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
open class BugReportStateProvider : PreviewParameterProvider<BugReportState> {
override val values: Sequence<BugReportState>
get() = sequenceOf(
aBugReportState(),
aBugReportState().copy(
formState = BugReportFormState.Default.copy(
description = "A long enough description",
sendScreenshot = true,
),
hasCrashLogs = true,
screenshotUri = "aUri"
),
aBugReportState().copy(sending = Async.Loading()),
aBugReportState().copy(sending = Async.Success(Unit)),
)
}
fun aBugReportState() = BugReportState(
formState = BugReportFormState.Default,
hasCrashLogs = false,
screenshotUri = null,
sendingProgress = 0F,
sending = Async.Uninitialized,
eventSink = {}
)

View file

@ -40,6 +40,7 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.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)
}

View file

@ -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)
)
}

View file

@ -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
)

View file

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

View file

@ -21,9 +21,9 @@ import io.element.android.features.rageshake.preferences.RageshakePreferencesSta
@Stable
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
)

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.rageshake.detection
import io.element.android.features.rageshake.preferences.aRageshakePreferencesState
fun aRageshakeDetectionState() = RageshakeDetectionState(
takeScreenshot = false,
showDialog = false,
isStarted = false,
preferenceState = aRageshakePreferencesState(),
eventSink = {}
)

View file

@ -101,11 +101,11 @@ fun RageshakeDialogContent(
@Preview
@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() {

View file

@ -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,
)

View file

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

View file

@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.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)
}

View file

@ -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
)

View file

@ -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)
}

View file

@ -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()),
)
}

View file

@ -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)
}

View file

@ -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,
)

View file

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

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.model
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.RoomId
open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> {
override val values: Sequence<RoomListRoomSummary>
get() = sequenceOf(
aRoomListRoomSummary(),
aRoomListRoomSummary().copy(lastMessage = null),
aRoomListRoomSummary().copy(hasUnread = true),
aRoomListRoomSummary().copy(timestamp = "88:88"),
aRoomListRoomSummary().copy(timestamp = "88:88", hasUnread = true),
aRoomListRoomSummary().copy(isPlaceholder = true),
)
}
fun aRoomListRoomSummary() = RoomListRoomSummary(
id = "!roomId",
roomId = RoomId("!roomId"),
name = "Room name",
hasUnread = false,
timestamp = null,
lastMessage = "Last message",
avatarData = AvatarData("!roomId", "Room name"),
isPlaceholder = false,
)

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* 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")

View file

@ -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,
)

View file

@ -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
)

View file

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

View file

@ -22,6 +22,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.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,
)
}

View file

@ -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"

View file

@ -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(),
)
}

View file

@ -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() {

View file

@ -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() {

View file

@ -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)
}

View file

@ -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()
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.avatar
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class AvatarDataProvider : PreviewParameterProvider<AvatarData> {
override val values: Sequence<AvatarData>
get() = sequenceOf(
anAvatarData(),
anAvatarData().copy(name = null),
anAvatarData().copy(url = "aUrl"),
)
}
fun anAvatarData() = AvatarData(
// Let's the id not start with a 'a'.
id = "@id_of_alice:server.org",
name = "Alice",
)

View file

@ -117,11 +117,11 @@ fun ConfirmationDialog(
@Preview
@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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.components.preferences.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class ImageVectorProvider : PreviewParameterProvider<ImageVector?> {
override val values: Sequence<ImageVector?>
get() = sequenceOf(
null,
Icons.Default.BugReport,
)
}

View file

@ -22,7 +22,11 @@ import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import 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.toEnabledColor
@ -45,3 +49,18 @@ fun PreferenceIcon(
Spacer(modifier = modifier.width(56.dp))
}
}
@Preview
@Composable
internal fun PreferenceIconLightPreview(@PreviewParameter(ImageVectorProvider::class) content: ImageVector?) =
ElementPreviewLight { ContentToPreview(content) }
@Preview
@Composable
internal fun PreferenceIconDarkPreview(@PreviewParameter(ImageVectorProvider::class) content: ImageVector?) =
ElementPreviewDark { ContentToPreview(content) }
@Composable
private fun ContentToPreview(content: ImageVector?) {
PreferenceIcon(content)
}

View file

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

View file

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

View file

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

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