diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b8549fc42..1df2119bf9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APK diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index e85fdc31d9..65f3cbb53c 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -62,7 +62,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Dependency analysis diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 80ab156480..9c0aac7aef 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -40,7 +40,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run code quality check suite diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index 63b72b3318..e9ea93619c 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -24,7 +24,7 @@ jobs: java-version: '17' # Add gradle cache, this should speed up the process - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Record screenshots diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 225dc4905e..7e535a1467 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 - name: Create app bundle env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 1cbbfb639e..42846c2cd5 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -32,7 +32,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: 🔊 Publish results to Sonar diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0094f34e4c..460e57b4e3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,7 +34,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} diff --git a/CHANGES.md b/CHANGES.md index 03766ed66b..c9e3ad2d33 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,44 @@ +Changes in Element X v0.1.5 (2023-08-28) +======================================== + +Bugfixes 🐛 +---------- + - Fix crash when opening any room. ([#1160](https://github.com/vector-im/element-x-android/issues/1160)) + + +Changes in Element X v0.1.4 (2023-08-28) +======================================== + +Features ✨ +---------- + - Allow cancelling media upload ([#769](https://github.com/vector-im/element-x-android/issues/769)) + - Enable OIDC support. ([#1127](https://github.com/vector-im/element-x-android/issues/1127)) + - Add a "Setting up account" screen, displayed the first time the user logs in to the app (per account). ([#1149](https://github.com/vector-im/element-x-android/issues/1149)) + +Bugfixes 🐛 +---------- + - Videos sent from the app were cropped in some cases. ([#862](https://github.com/vector-im/element-x-android/issues/862)) + - Timeline: sender names are now displayed in one single line. ([#1033](https://github.com/vector-im/element-x-android/issues/1033)) + - Fix `TextButtons` being displayed in black. ([#1077](https://github.com/vector-im/element-x-android/issues/1077)) + - Linkify links in HTML contents. ([#1079](https://github.com/vector-im/element-x-android/issues/1079)) + - Fix bug reporter failing after not finding some log files. ([#1082](https://github.com/vector-im/element-x-android/issues/1082)) + - Fix rendering of inline elements in list items. ([#1090](https://github.com/vector-im/element-x-android/issues/1090)) + - Fix crash RuntimeException "No matching key found for the ciphertext in the stream" ([#1101](https://github.com/vector-im/element-x-android/issues/1101)) + - Make links in messages clickable again. ([#1111](https://github.com/vector-im/element-x-android/issues/1111)) + - When event has no id, just cancel parsing the latest room message for a room. ([#1125](https://github.com/vector-im/element-x-android/issues/1125)) + - Only display verification prompt after initial sync is done. ([#1131](https://github.com/vector-im/element-x-android/issues/1131)) + +In development 🚧 +---------------- + - [Poll] Add feature flag in developer options ([#1064](https://github.com/vector-im/element-x-android/issues/1064)) + - [Polls] Improve UI and render ended state ([#1113](https://github.com/vector-im/element-x-android/issues/1113)) + +Other changes +------------- + - Compound: add `ListItem` and `ListSectionHeader` components. ([#990](https://github.com/vector-im/element-x-android/issues/990)) + - Migrate `object` to `data object` in sealed interface / class #1135 ([#1135](https://github.com/vector-im/element-x-android/issues/1135)) + + Changes in Element X v0.1.2 (2023-08-16) ======================================== diff --git a/README.md b/README.md index f24efcd828..2a0d352187 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ If after your research you still have a question, ask at [#element-x-android:mat ## Copyright & License -Copyright (c) 2022 New Vector Ltd +Copyright © New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the [LICENSE](LICENSE) file, or at: diff --git a/app/src/main/kotlin/io/element/android/x/MainNode.kt b/app/src/main/kotlin/io/element/android/x/MainNode.kt index 8e7d0f194d..ce894ec2d6 100644 --- a/app/src/main/kotlin/io/element/android/x/MainNode.kt +++ b/app/src/main/kotlin/io/element/android/x/MainNode.kt @@ -27,9 +27,9 @@ import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.plugin.Plugin -import io.element.android.appnav.LoggedInFlowNode -import io.element.android.appnav.room.RoomLoadedFlowNode +import io.element.android.appnav.LoggedInAppScopeFlowNode import io.element.android.appnav.RootFlowNode +import io.element.android.appnav.room.RoomLoadedFlowNode import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.DaggerComponentOwner @@ -45,18 +45,17 @@ class MainNode( buildContext: BuildContext, private val mainDaggerComponentOwner: MainDaggerComponentsOwner, plugins: List, -) : - ParentNode( - navModel = PermanentNavModel( - navTargets = setOf(RootNavTarget), - savedStateMap = buildContext.savedStateMap, - ), - buildContext = buildContext, - plugins = plugins, +) : ParentNode( + navModel = PermanentNavModel( + navTargets = setOf(RootNavTarget), + savedStateMap = buildContext.savedStateMap, ), + buildContext = buildContext, + plugins = plugins, +), DaggerComponentOwner by mainDaggerComponentOwner { - private val loggedInFlowNodeCallback = object : LoggedInFlowNode.LifecycleCallback { + private val loggedInFlowNodeCallback = object : LoggedInAppScopeFlowNode.LifecycleCallback { override fun onFlowCreated(identifier: String, client: MatrixClient) { val component = bindings().sessionComponentBuilder().client(client).build() mainDaggerComponentOwner.addComponent(identifier, component) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt new file mode 100644 index 0000000000..6a0e60aaaf --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt @@ -0,0 +1,115 @@ +/* + * 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.appnav + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import coil.Coil +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.lifecycle.subscribe +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.ParentNode +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.architecture.waitForChildAttached +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.ui.di.MatrixUIBindings +import kotlinx.parcelize.Parcelize + +/** + * `LoggedInAppScopeFlowNode` is a Node responsible to set up the Dagger + * [io.element.android.libraries.di.SessionScope]. It has only one child: [LoggedInFlowNode]. + * This allow to inject objects with SessionScope in the constructor of [LoggedInFlowNode]. + */ +@ContributesNode(AppScope::class) +class LoggedInAppScopeFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : ParentNode( + navModel = PermanentNavModel( + navTargets = setOf(NavTarget), + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { + interface Callback : Plugin { + fun onOpenBugReport() + } + + @Parcelize + object NavTarget : Parcelable + + interface LifecycleCallback : NodeLifecycleCallback { + fun onFlowCreated(identifier: String, client: MatrixClient) + + fun onFlowReleased(identifier: String, client: MatrixClient) + } + + data class Inputs( + val matrixClient: MatrixClient + ) : NodeInputs + + private val inputs: Inputs = inputs() + + override fun onBuilt() { + super.onBuilt() + lifecycle.subscribe( + onCreate = { + plugins().forEach { it.onFlowCreated(id, inputs.matrixClient) } + val imageLoaderFactory = bindings().loggedInImageLoaderFactory() + Coil.setImageLoader(imageLoaderFactory) + }, + onDestroy = { + plugins().forEach { it.onFlowReleased(id, inputs.matrixClient) } + } + ) + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + val callback = object : LoggedInFlowNode.Callback { + override fun onOpenBugReport() { + plugins().forEach { it.onOpenBugReport() } + } + } + val nodeLifecycleCallbacks = plugins() + return createNode(buildContext, nodeLifecycleCallbacks + callback) + } + + suspend fun attachSession(): LoggedInFlowNode { + return waitForChildAttached { _ -> true } + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = navModel, + modifier = modifier, + ) + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 7943151a5e..5006218bda 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import coil.Coil import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext @@ -53,19 +52,15 @@ import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.libraries.architecture.BackstackNode -import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler -import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.architecture.inputs import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.designsystem.utils.SnackbarDispatcher -import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.sync.SyncState -import io.element.android.libraries.matrix.ui.di.MatrixUIBindings import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope @@ -76,7 +71,7 @@ import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import timber.log.Timber -@ContributesNode(AppScope::class) +@ContributesNode(SessionScope::class) class LoggedInFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, @@ -91,6 +86,7 @@ class LoggedInFlowNode @AssistedInject constructor( private val networkMonitor: NetworkMonitor, private val notificationDrawerManager: NotificationDrawerManager, private val ftueState: FtueState, + private val matrixClient: MatrixClient, snackbarDispatcher: SnackbarDispatcher, ) : BackstackNode( backstack = BackStack( @@ -105,32 +101,18 @@ class LoggedInFlowNode @AssistedInject constructor( fun onOpenBugReport() } - interface LifecycleCallback : NodeLifecycleCallback { - fun onFlowCreated(identifier: String, client: MatrixClient) - - fun onFlowReleased(identifier: String, client: MatrixClient) - } - - data class Inputs( - val matrixClient: MatrixClient - ) : NodeInputs - - private val inputs: Inputs = inputs() - private val syncService = inputs.matrixClient.syncService() + private val syncService = matrixClient.syncService() private val loggedInFlowProcessor = LoggedInEventProcessor( snackbarDispatcher, - inputs.matrixClient.roomMembershipObserver(), - inputs.matrixClient.sessionVerificationService(), + matrixClient.roomMembershipObserver(), + matrixClient.sessionVerificationService(), ) override fun onBuilt() { super.onBuilt() lifecycle.subscribe( onCreate = { - plugins().forEach { it.onFlowCreated(id, inputs.matrixClient) } - val imageLoaderFactory = bindings().loggedInImageLoaderFactory() - Coil.setImageLoader(imageLoaderFactory) - appNavigationStateService.onNavigateToSession(id, inputs.matrixClient.sessionId) + appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId) // TODO We do not support Space yet, so directly navigate to main space appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE) loggedInFlowProcessor.observeEvents(coroutineScope) @@ -146,7 +128,6 @@ class LoggedInFlowNode @AssistedInject constructor( } }, onDestroy = { - plugins().forEach { it.onFlowReleased(id, inputs.matrixClient) } appNavigationStateService.onLeavingSpace(id) appNavigationStateService.onLeavingSession(id) loggedInFlowProcessor.stopObserving() @@ -178,10 +159,10 @@ class LoggedInFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - object Permanent : NavTarget + data object Permanent : NavTarget @Parcelize - object RoomList : NavTarget + data object RoomList : NavTarget @Parcelize data class Room( @@ -190,19 +171,19 @@ class LoggedInFlowNode @AssistedInject constructor( ) : NavTarget @Parcelize - object Settings : NavTarget + data object Settings : NavTarget @Parcelize - object CreateRoom : NavTarget + data object CreateRoom : NavTarget @Parcelize - object VerifySession : NavTarget + data object VerifySession : NavTarget @Parcelize - object InviteList : NavTarget + data object InviteList : NavTarget @Parcelize - object Ftue : NavTarget + data object Ftue : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -351,4 +332,3 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push(NavTarget.InviteList) } } - diff --git a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt index 1ed1aec678..17f3a44eb8 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt @@ -64,7 +64,7 @@ class NotLoggedInFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - object OnBoarding : NavTarget + data object OnBoarding : NavTarget @Parcelize data class LoginFlow( diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 089e956c61..85032e751f 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -72,15 +72,14 @@ class RootFlowNode @AssistedInject constructor( private val bugReportEntryPoint: BugReportEntryPoint, private val intentResolver: IntentResolver, private val oidcActionFlow: OidcActionFlow, -) : - BackstackNode( - backstack = BackStack( - initialElement = NavTarget.SplashScreen, - savedStateMap = buildContext.savedStateMap, - ), - buildContext = buildContext, - plugins = plugins - ) { +) : BackstackNode( + backstack = BackStack( + initialElement = NavTarget.SplashScreen, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { override fun onBuilt() { matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap) @@ -170,10 +169,10 @@ class RootFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - object SplashScreen : NavTarget + data object SplashScreen : NavTarget @Parcelize - object NotLoggedInFlow : NavTarget + data object NotLoggedInFlow : NavTarget @Parcelize data class LoggedInFlow( @@ -182,7 +181,7 @@ class RootFlowNode @AssistedInject constructor( ) : NavTarget @Parcelize - object BugReport : NavTarget + data object BugReport : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -191,14 +190,14 @@ class RootFlowNode @AssistedInject constructor( val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also { Timber.w("Couldn't find any session, go through SplashScreen") } - val inputs = LoggedInFlowNode.Inputs(matrixClient) - val callback = object : LoggedInFlowNode.Callback { + val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient) + val callback = object : LoggedInAppScopeFlowNode.Callback { override fun onOpenBugReport() { backstack.push(NavTarget.BugReport) } } val nodeLifecycleCallbacks = plugins() - createNode(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks) + createNode(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks) } NavTarget.NotLoggedInFlow -> createNode(buildContext) NavTarget.SplashScreen -> splashNode(buildContext) @@ -233,6 +232,7 @@ class RootFlowNode @AssistedInject constructor( private suspend fun navigateTo(deeplinkData: DeeplinkData) { Timber.d("Navigating to $deeplinkData") attachSession(deeplinkData.sessionId) + .attachSession() .apply { when (deeplinkData) { is DeeplinkData.Root -> attachRoot() @@ -246,7 +246,7 @@ class RootFlowNode @AssistedInject constructor( oidcActionFlow.post(oidcAction) } - private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode { + private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode { //TODO handle multi-session return waitForChildAttached { navTarget -> navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt index 664ec1f663..be784ea7c9 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt @@ -17,5 +17,5 @@ package io.element.android.appnav.loggedin // sealed interface LoggedInEvents { -// object MyEvent : LoggedInEvents +// data object MyEvent : LoggedInEvents // } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomState.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomState.kt index db4627c3b4..3836fbff74 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomState.kt @@ -32,8 +32,8 @@ import kotlinx.coroutines.flow.stateIn import javax.inject.Inject sealed interface LoadingRoomState { - object Loading : LoadingRoomState - object Error : LoadingRoomState + data object Loading : LoadingRoomState + data object Error : LoadingRoomState data class Loaded(val room: MatrixRoom) : LoadingRoomState } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index 661d3c5433..f8fa7e629f 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -77,10 +77,10 @@ class RoomFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - object Loading : NavTarget + data object Loading : NavTarget @Parcelize - object Loaded : NavTarget + data object Loaded : NavTarget } override fun onBuilt() { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt index d00c4791f7..8230e62119 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt @@ -152,10 +152,10 @@ class RoomLoadedFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - object Messages : NavTarget + data object Messages : NavTarget @Parcelize - object RoomDetails : NavTarget + data object RoomDetails : NavTarget @Parcelize data class RoomMemberDetails(val userId: UserId) : NavTarget diff --git a/build.gradle.kts b/build.gradle.kts index 556ee5ff00..b48ef70ce7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient buildscript { dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") classpath("com.google.gms:google-services:4.3.15") } } diff --git a/changelog.d/1033.bugfix b/changelog.d/1033.bugfix deleted file mode 100644 index db4397449b..0000000000 --- a/changelog.d/1033.bugfix +++ /dev/null @@ -1 +0,0 @@ -Timeline: sender names are now displayed in one single line. diff --git a/changelog.d/1064.wip b/changelog.d/1064.wip deleted file mode 100644 index f3d8af5133..0000000000 --- a/changelog.d/1064.wip +++ /dev/null @@ -1 +0,0 @@ -[Poll] Add feature flag in developer options diff --git a/changelog.d/1077.bugfix b/changelog.d/1077.bugfix deleted file mode 100644 index f76a12e5a2..0000000000 --- a/changelog.d/1077.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `TextButtons` being displayed in black. diff --git a/changelog.d/1079.bugfix b/changelog.d/1079.bugfix deleted file mode 100644 index 6fcaa759c3..0000000000 --- a/changelog.d/1079.bugfix +++ /dev/null @@ -1 +0,0 @@ -Linkify links in HTML contents. diff --git a/changelog.d/1082.bugfix b/changelog.d/1082.bugfix deleted file mode 100644 index c279e09af2..0000000000 --- a/changelog.d/1082.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug reporter failing after not finding some log files. diff --git a/changelog.d/1090.bugfix b/changelog.d/1090.bugfix deleted file mode 100644 index 7c93ed6879..0000000000 --- a/changelog.d/1090.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix rendering of inline elements in list items. diff --git a/changelog.d/1101.bugfix b/changelog.d/1101.bugfix deleted file mode 100644 index 79c25ffaf6..0000000000 --- a/changelog.d/1101.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix crash RuntimeException "No matching key found for the ciphertext in the stream" diff --git a/changelog.d/1111.bugfix b/changelog.d/1111.bugfix deleted file mode 100644 index 4d1baf87bf..0000000000 --- a/changelog.d/1111.bugfix +++ /dev/null @@ -1 +0,0 @@ -Make links in messages clickable again. diff --git a/changelog.d/1125.bugfix b/changelog.d/1125.bugfix deleted file mode 100644 index 85ebb87033..0000000000 --- a/changelog.d/1125.bugfix +++ /dev/null @@ -1 +0,0 @@ -When event has no id, just cancel parsing the latest room message for a room. diff --git a/changelog.d/1127.feature b/changelog.d/1127.feature deleted file mode 100644 index da2c6b3cf1..0000000000 --- a/changelog.d/1127.feature +++ /dev/null @@ -1 +0,0 @@ -Enable OIDC support. diff --git a/changelog.d/1131.bugfix b/changelog.d/1131.bugfix deleted file mode 100644 index 23649a19d0..0000000000 --- a/changelog.d/1131.bugfix +++ /dev/null @@ -1 +0,0 @@ -Only display verification prompt after initial sync is done. diff --git a/changelog.d/1143.feature b/changelog.d/1143.feature new file mode 100644 index 0000000000..84a86f4f25 --- /dev/null +++ b/changelog.d/1143.feature @@ -0,0 +1 @@ +Create poll. diff --git a/changelog.d/1168.bugfix b/changelog.d/1168.bugfix new file mode 100644 index 0000000000..f7f959ac0a --- /dev/null +++ b/changelog.d/1168.bugfix @@ -0,0 +1 @@ +Bug reporter crashes when 'send logs' is disabled. diff --git a/changelog.d/1177.bugfix b/changelog.d/1177.bugfix new file mode 100644 index 0000000000..edbf2e9006 --- /dev/null +++ b/changelog.d/1177.bugfix @@ -0,0 +1 @@ +Add missing link to the terms on the analytics setting screen. diff --git a/changelog.d/1187.misc b/changelog.d/1187.misc new file mode 100644 index 0000000000..301e3a6fc4 --- /dev/null +++ b/changelog.d/1187.misc @@ -0,0 +1 @@ +Remove unnecessary year in copyright mention. diff --git a/changelog.d/769.feature b/changelog.d/769.feature deleted file mode 100644 index 8df765c27c..0000000000 --- a/changelog.d/769.feature +++ /dev/null @@ -1 +0,0 @@ -Allow cancelling media upload diff --git a/changelog.d/862.bugfix b/changelog.d/862.bugfix deleted file mode 100644 index 30715a76e3..0000000000 --- a/changelog.d/862.bugfix +++ /dev/null @@ -1 +0,0 @@ -Videos sent from the app were cropped in some cases. diff --git a/changelog.d/928.bugfix b/changelog.d/928.bugfix new file mode 100644 index 0000000000..98a4cd34e0 --- /dev/null +++ b/changelog.d/928.bugfix @@ -0,0 +1 @@ +Make sure Snackbars are only displayed once. diff --git a/changelog.d/990.misc b/changelog.d/990.misc deleted file mode 100644 index 0deaf51c89..0000000000 --- a/changelog.d/990.misc +++ /dev/null @@ -1 +0,0 @@ -Compound: add `ListItem` and `ListSectionHeader` components. diff --git a/fastlane/metadata/android/en-US/changelogs/40001040.txt b/fastlane/metadata/android/en-US/changelogs/40001040.txt new file mode 100644 index 0000000000..2593704785 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40001040.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and add OIDC support. +Full changelog: https://github.com/vector-im/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/40001050.txt b/fastlane/metadata/android/en-US/changelogs/40001050.txt new file mode 100644 index 0000000000..2593704785 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40001050.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and add OIDC support. +Full changelog: https://github.com/vector-im/element-x-android/releases diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt index 7cf0f51dfd..e03796297e 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt @@ -21,5 +21,6 @@ import io.element.android.features.analytics.api.AnalyticsOptInEvents data class AnalyticsPreferencesState( val applicationName: String, val isEnabled: Boolean, + val policyUrl: String, val eventSink: (AnalyticsOptInEvents) -> Unit, ) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt index ea397b4d67..18f902a6fd 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt @@ -28,5 +28,6 @@ open class AnalyticsPreferencesStateProvider : PreviewParameterProvider onClickTerms() } + }, modifier = Modifier - .clip(shape = RoundedCornerShape(8.dp)) - .clickable { onClickTerms() } .padding(8.dp), - style = ElementTheme.typography.fontBodyMdRegular, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.secondary, + style = ElementTheme.typography.fontBodyMdRegular + .copy( + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.Center, + ) ) } } diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/DefaultAnalyticsPreferencesPresenter.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/DefaultAnalyticsPreferencesPresenter.kt index 6debe4c232..06431402a6 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/DefaultAnalyticsPreferencesPresenter.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/DefaultAnalyticsPreferencesPresenter.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.rememberCoroutineScope import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.analytics.api.AnalyticsOptInEvents +import io.element.android.features.analytics.api.Config import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState import io.element.android.libraries.core.meta.BuildMeta @@ -51,6 +52,7 @@ class DefaultAnalyticsPreferencesPresenter @Inject constructor( return AnalyticsPreferencesState( applicationName = buildMeta.applicationName, isEnabled = isEnabled.value, + policyUrl = Config.POLICY_LINK, eventSink = ::handleEvents ) } diff --git a/features/analytics/impl/src/main/res/values-de/translations.xml b/features/analytics/impl/src/main/res/values-de/translations.xml index 44890927a4..7ef2ff2500 100644 --- a/features/analytics/impl/src/main/res/values-de/translations.xml +++ b/features/analytics/impl/src/main/res/values-de/translations.xml @@ -5,6 +5,6 @@ "Du kannst alle unsere Nutzerbedingungen %1$s lesen." "hier" "Du kannst dies jederzeit deaktivieren" - "Wir geben ""keine"" Informationen an Dritte weiter" + "Wir geben deine Daten nicht an Dritte weiter" "Hilf uns, %1$s zu verbessern" diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt index 541e5858bd..b9cb4d95ff 100644 --- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt +++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt @@ -21,8 +21,8 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.analytics.api.AnalyticsOptInEvents -import io.element.android.features.analytics.test.FakeAnalyticsService import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.services.analytics.test.FakeAnalyticsService import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt index d0914932bb..29c4579d8f 100644 --- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt +++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt @@ -21,8 +21,8 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.analytics.api.AnalyticsOptInEvents -import io.element.android.features.analytics.test.FakeAnalyticsService import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.services.analytics.test.FakeAnalyticsService import kotlinx.coroutines.test.runTest import org.junit.Test @@ -39,6 +39,7 @@ class AnalyticsPreferencesPresenterTest { skipItems(1) val initialState = awaitItem() assertThat(initialState.isEnabled).isTrue() + assertThat(initialState.policyUrl).isNotEmpty() } } diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index fe2970dd80..0ef46a57ec 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -60,7 +60,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(libs.test.robolectric) - testImplementation(projects.features.analytics.test) + testImplementation(projects.services.analytics.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.mediapickers.test) testImplementation(projects.libraries.mediaupload.test) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt index a5a78e54d5..3b96ac3edd 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt @@ -63,10 +63,10 @@ class ConfigureRoomFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - object Root : NavTarget + data object Root : NavTarget @Parcelize - object ConfigureRoom : NavTarget + data object ConfigureRoom : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt index 6f447e6bc9..207ab73e66 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt @@ -54,10 +54,10 @@ class CreateRoomFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - object Root : NavTarget + data object Root : NavTarget @Parcelize - object NewRoom : NavTarget + data object NewRoom : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index a020b387cb..f5dcfd8451 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -27,5 +27,5 @@ sealed interface ConfigureRoomEvents { data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents - object CancelCreateRoom : ConfigureRoomEvents + data object CancelCreateRoom : ConfigureRoomEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt index 7d8211aea5..b22489dd2d 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootEvents.kt @@ -20,5 +20,5 @@ import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface CreateRoomRootEvents { data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents - object CancelStartDM : CreateRoomRootEvents + data object CancelStartDM : CreateRoomRootEvents } diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index 1703cf4142..864d85423d 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -22,7 +22,6 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.CreatedRoom -import io.element.android.features.analytics.test.FakeAnalyticsService import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.createroom.impl.userlist.UserListDataStore @@ -38,6 +37,7 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.services.analytics.test.FakeAnalyticsService import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index 002e8c77fd..0f88280c84 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -21,7 +21,6 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.CreatedRoom -import io.element.android.features.analytics.test.FakeAnalyticsService import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory import io.element.android.features.createroom.impl.userlist.UserListDataStore @@ -35,6 +34,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.usersearch.test.FakeUserRepository +import io.element.android.services.analytics.test.FakeAnalyticsService import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest import org.junit.Before diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts index 0dee792464..8b12767b20 100644 --- a/features/ftue/impl/build.gradle.kts +++ b/features/ftue/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) api(projects.features.ftue.api) + implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) @@ -49,7 +50,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.features.analytics.test) + testImplementation(projects.services.analytics.test) ksp(libs.showkase.processor) } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt index 0ff9c80d46..2b515c18a6 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt @@ -34,6 +34,7 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.analytics.api.AnalyticsEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint +import io.element.android.features.ftue.impl.migration.MigrationScreenNode import io.element.android.features.ftue.impl.state.DefaultFtueState import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.ftue.impl.welcome.WelcomeNode @@ -41,6 +42,7 @@ import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SessionScope import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -50,7 +52,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize -@ContributesNode(AppScope::class) +@ContributesNode(SessionScope::class) class FtueFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, @@ -69,13 +71,16 @@ class FtueFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - object Placeholder : NavTarget + data object Placeholder : NavTarget @Parcelize - object WelcomeScreen : NavTarget + data object MigrationScreen : NavTarget @Parcelize - object AnalyticsOptIn : NavTarget + data object WelcomeScreen : NavTarget + + @Parcelize + data object AnalyticsOptIn : NavTarget } private val callback = plugins.filterIsInstance().firstOrNull() @@ -102,6 +107,14 @@ class FtueFlowNode @AssistedInject constructor( NavTarget.Placeholder -> { createNode(buildContext) } + NavTarget.MigrationScreen -> { + val callback = object : MigrationScreenNode.Callback { + override fun onMigrationFinished() { + lifecycleScope.launch { moveToNextStep() } + } + } + createNode(buildContext, listOf(callback)) + } NavTarget.WelcomeScreen -> { val callback = object : WelcomeNode.Callback { override fun onContinueClicked() { @@ -117,12 +130,15 @@ class FtueFlowNode @AssistedInject constructor( } } - private suspend fun moveToNextStep() { + private fun moveToNextStep() { when (ftueState.getNextStep()) { - is FtueStep.WelcomeScreen -> { + FtueStep.MigrationScreen -> { + backstack.newRoot(NavTarget.MigrationScreen) + } + FtueStep.WelcomeScreen -> { backstack.newRoot(NavTarget.WelcomeScreen) } - is FtueStep.AnalyticsOptIn -> { + FtueStep.AnalyticsOptIn -> { backstack.replace(NavTarget.AnalyticsOptIn) } null -> callback?.onFtueFlowFinished() diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenNode.kt similarity index 50% rename from features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollFlowNode.kt rename to features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenNode.kt index 9dfeebc692..a4c5d16bc4 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenNode.kt @@ -14,57 +14,40 @@ * limitations under the License. */ -package io.element.android.features.poll.impl +package io.element.android.features.ftue.impl.migration -import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin -import com.bumble.appyx.navmodel.backstack.BackStack import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.architecture.BackstackNode -import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler -import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope -import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -class PollFlowNode @AssistedInject constructor( +class MigrationScreenNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, -) : BackstackNode( - backstack = BackStack( - initialElement = NavTarget.Root, - savedStateMap = buildContext.savedStateMap, - ), - buildContext = buildContext, - plugins = plugins, -) { + private val presenter: MigrationScreenPresenter, +) : Node(buildContext, plugins = plugins) { - sealed interface NavTarget : Parcelable { - @Parcelize - object Root : NavTarget + interface Callback : Plugin { + fun onMigrationFinished() } - override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { - return when (navTarget) { - NavTarget.Root -> { - createNode(buildContext) - } - } + private fun onMigrationFinished() { + plugins.filterIsInstance().forEach { it.onMigrationFinished() } } @Composable override fun View(modifier: Modifier) { - Children( - navModel = backstack, - modifier = modifier, - transitionHandler = rememberDefaultTransitionHandler(), + val state = presenter.present() + MigrationScreenView( + state, + onMigrationFinished = ::onMigrationFinished, + modifier = modifier ) } } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenPresenter.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenPresenter.kt new file mode 100644 index 0000000000..6507130383 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenPresenter.kt @@ -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.features.ftue.impl.migration + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import javax.inject.Inject + +class MigrationScreenPresenter @Inject constructor( + private val matrixClient: MatrixClient, + private val migrationScreenStore: MigrationScreenStore, +) : Presenter { + @Composable + override fun present(): MigrationScreenState { + val roomListState by matrixClient.roomListService.state.collectAsState() + if (roomListState == RoomListService.State.Running) { + LaunchedEffect(Unit) { + migrationScreenStore.setMigrationScreenShown(matrixClient.sessionId) + } + } + return MigrationScreenState( + isMigrating = roomListState != RoomListService.State.Running + ) + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenState.kt new file mode 100644 index 0000000000..fa718990e4 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenState.kt @@ -0,0 +1,21 @@ +/* + * 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.ftue.impl.migration + +data class MigrationScreenState( + val isMigrating: Boolean +) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenStore.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenStore.kt new file mode 100644 index 0000000000..42eeab673a --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenStore.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.ftue.impl.migration + +import io.element.android.libraries.matrix.api.core.SessionId + +interface MigrationScreenStore { + fun isMigrationScreenNeeded(sessionId: SessionId): Boolean + fun setMigrationScreenShown(sessionId: SessionId) + fun reset() +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenView.kt new file mode 100644 index 0000000000..a8f20713e4 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenView.kt @@ -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.ftue.impl.migration + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.features.ftue.impl.R +import io.element.android.libraries.designsystem.atomic.pages.SunsetPage +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview + +@Composable +fun MigrationScreenView( + migrationState: MigrationScreenState, + onMigrationFinished: () -> Unit, + modifier: Modifier = Modifier, +) { + if (migrationState.isMigrating.not()) { + LaunchedEffect(Unit) { + onMigrationFinished() + } + } + SunsetPage( + modifier = modifier, + isLoading = true, + title = stringResource(id = R.string.screen_migration_title), + subtitle = stringResource(id = R.string.screen_migration_message), + overallContent = {} + ) +} + +@DayNightPreviews +@Composable +internal fun MigrationViewPreview() = ElementPreview { + MigrationScreenView( + migrationState = MigrationScreenState(isMigrating = true), + onMigrationFinished = {}) +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/SharedPrefsMigrationScreenStore.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/SharedPrefsMigrationScreenStore.kt new file mode 100644 index 0000000000..c39a535e7d --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/migration/SharedPrefsMigrationScreenStore.kt @@ -0,0 +1,61 @@ +/* + * 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.ftue.impl.migration + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.androidutils.hash.hash +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.DefaultPreferences +import io.element.android.libraries.matrix.api.core.SessionId +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class SharedPrefsMigrationScreenStore @Inject constructor( + @DefaultPreferences private val sharedPreferences: SharedPreferences, +) : MigrationScreenStore { + + override fun isMigrationScreenNeeded(sessionId: SessionId): Boolean { + return sharedPreferences.getBoolean(sessionId.toKey(), false).not() + } + + override fun setMigrationScreenShown(sessionId: SessionId) { + sharedPreferences.edit().putBoolean(sessionId.toKey(), true).apply() + } + + override fun reset() { + sharedPreferences.edit { + sharedPreferences.all.keys + .filter { it.startsWith(IS_MIGRATION_SCREEN_SHOWN_PREFIX) } + .forEach { + remove(it) + } + } + } + + private fun SessionId.toKey(): String { + // Hash the sessionId to get rid of exotic char and take only the first 16 chars, + // The risk of collision is not high. + return IS_MIGRATION_SCREEN_SHOWN_PREFIX + value.hash().take(16) + } + + companion object { + private const val IS_MIGRATION_SCREEN_SHOWN_PREFIX = "is_migration_screen_shown_" + } +} + diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt index 52c8d90254..108072cba9 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt @@ -19,8 +19,10 @@ package io.element.android.features.ftue.impl.state import androidx.annotation.VisibleForTesting import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.ftue.api.state.FtueState +import io.element.android.features.ftue.impl.migration.MigrationScreenStore import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState -import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -30,11 +32,13 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking import javax.inject.Inject -@ContributesBinding(AppScope::class) +@ContributesBinding(SessionScope::class) class DefaultFtueState @Inject constructor( private val coroutineScope: CoroutineScope, private val analyticsService: AnalyticsService, private val welcomeScreenState: WelcomeScreenState, + private val migrationScreenStore: MigrationScreenStore, + private val matrixClient: MatrixClient, ) : FtueState { override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete()) @@ -42,6 +46,7 @@ class DefaultFtueState @Inject constructor( override suspend fun reset() { welcomeScreenState.reset() analyticsService.reset() + migrationScreenStore.reset() } init { @@ -52,7 +57,10 @@ class DefaultFtueState @Inject constructor( fun getNextStep(currentStep: FtueStep? = null): FtueStep? = when (currentStep) { - null -> if (shouldDisplayWelcomeScreen()) FtueStep.WelcomeScreen else getNextStep( + null -> if (shouldDisplayMigrationScreen()) FtueStep.MigrationScreen else getNextStep( + FtueStep.MigrationScreen + ) + FtueStep.MigrationScreen -> if (shouldDisplayWelcomeScreen()) FtueStep.WelcomeScreen else getNextStep( FtueStep.WelcomeScreen ) FtueStep.WelcomeScreen -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep( @@ -63,11 +71,16 @@ class DefaultFtueState @Inject constructor( private fun isAnyStepIncomplete(): Boolean { return listOf( + shouldDisplayMigrationScreen(), shouldDisplayWelcomeScreen(), needsAnalyticsOptIn() ).any { it } } + private fun shouldDisplayMigrationScreen(): Boolean { + return migrationScreenStore.isMigrationScreenNeeded(matrixClient.sessionId) + } + private fun needsAnalyticsOptIn(): Boolean { // We need this function to not be suspend, so we need to load the value through runBlocking return runBlocking { analyticsService.didAskUserConsent().first().not() } @@ -89,6 +102,7 @@ class DefaultFtueState @Inject constructor( } sealed interface FtueStep { - object WelcomeScreen : FtueStep - object AnalyticsOptIn : FtueStep + data object MigrationScreen : FtueStep + data object WelcomeScreen : FtueStep + data object AnalyticsOptIn : FtueStep } diff --git a/features/ftue/impl/src/main/res/values-cs/translations.xml b/features/ftue/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..f1734c9c75 --- /dev/null +++ b/features/ftue/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,11 @@ + + + "Toto je jednorázový proces, děkujeme za čekání." + "Nastavení vašeho účtu" + "Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku." + "Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici." + "Rádi bychom se od vás dozvěděli, co si o tom myslíte, dejte nám vědět prostřednictvím stránky s nastavením." + "Jdeme na to!" + "Zde je to, co potřebujete vědět:" + "Vítá vás %1$s!" + diff --git a/features/ftue/impl/src/main/res/values-de/translations.xml b/features/ftue/impl/src/main/res/values-de/translations.xml index 0f2efc1c57..19b445bd50 100644 --- a/features/ftue/impl/src/main/res/values-de/translations.xml +++ b/features/ftue/impl/src/main/res/values-de/translations.xml @@ -1,6 +1,8 @@ - "Anrufe, Standortfreigabe, Suche und mehr werden später in diesem Jahr hinzugefügt." + "Dies ist ein einmaliger Vorgang, danke fürs Warten." + "Dein Konto einrichten" + "Anrufe, Umfragen, Suche und mehr werden später in diesem Jahr hinzugefügt." "Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein." "Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst." "Los geht\'s!" diff --git a/features/ftue/impl/src/main/res/values-fr/translations.xml b/features/ftue/impl/src/main/res/values-fr/translations.xml index 313a6a533f..9f431f545d 100644 --- a/features/ftue/impl/src/main/res/values-fr/translations.xml +++ b/features/ftue/impl/src/main/res/values-fr/translations.xml @@ -1,5 +1,7 @@ + "Ce processus n’a besoin d’être fait qu’une seule fois, merci de patienter." + "Configuration de votre compte." "L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour." "Nous serions ravis d’avoir votre avis, n’hésitez pas à nous le partager via la page des paramètres." "C’est parti !" diff --git a/features/ftue/impl/src/main/res/values-ru/translations.xml b/features/ftue/impl/src/main/res/values-ru/translations.xml index db4fcd21fc..d72497bf98 100644 --- a/features/ftue/impl/src/main/res/values-ru/translations.xml +++ b/features/ftue/impl/src/main/res/values-ru/translations.xml @@ -1,5 +1,7 @@ + "Это одноразовый процесс, спасибо, что подождали." + "Настройка учетной записи." "Звонки, опросы, поиск и многое другое будут добавлены позже в этом году." "История сообщений для зашифрованных комнат в этом обновлении будет недоступна." "Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек." diff --git a/features/ftue/impl/src/main/res/values-sk/translations.xml b/features/ftue/impl/src/main/res/values-sk/translations.xml index 2438518334..5bbd2d386d 100644 --- a/features/ftue/impl/src/main/res/values-sk/translations.xml +++ b/features/ftue/impl/src/main/res/values-sk/translations.xml @@ -1,5 +1,7 @@ + "Ide o jednorazový proces, ďakujeme za trpezlivosť." + "Nastavenie vášho účtu." "Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku." "História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii." "Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení." diff --git a/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml b/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml index 6c5d482cb8..b8b510aac5 100644 --- a/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,4 +1,5 @@ + "設定您的帳號" "開始吧!" diff --git a/features/ftue/impl/src/main/res/values/localazy.xml b/features/ftue/impl/src/main/res/values/localazy.xml index 05cc72034e..aee8470751 100644 --- a/features/ftue/impl/src/main/res/values/localazy.xml +++ b/features/ftue/impl/src/main/res/values/localazy.xml @@ -1,5 +1,7 @@ + "This is a one time process, thanks for waiting." + "Setting up your account." "Calls, polls, search and more will be added later this year." "Message history for encrypted rooms won’t be available in this update." "We’d love to hear from you, let us know what you think via the settings page." diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt index cfd489ea2a..f93f761994 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt @@ -17,11 +17,16 @@ package io.element.android.features.ftue.impl import com.google.common.truth.Truth.assertThat -import io.element.android.features.analytics.test.FakeAnalyticsService +import io.element.android.features.ftue.impl.migration.InMemoryMigrationScreenStore +import io.element.android.features.ftue.impl.migration.MigrationScreenStore import io.element.android.features.ftue.impl.state.DefaultFtueState import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analytics.test.FakeAnalyticsService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -45,12 +50,14 @@ class DefaultFtueStateTests { fun `given all checks being true, should display flow is false`() = runTest { val welcomeState = FakeWelcomeState() val analyticsService = FakeAnalyticsService() + val migrationScreenStore = InMemoryMigrationScreenStore() val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) - val state = createState(coroutineScope, welcomeState, analyticsService) + val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore) welcomeState.setWelcomeScreenShown() analyticsService.setDidAskUserConsent() + migrationScreenStore.setMigrationScreenShown(A_SESSION_ID) state.updateState() assertThat(state.shouldDisplayFlow.value).isFalse() @@ -63,16 +70,21 @@ class DefaultFtueStateTests { fun `traverse flow`() = runTest { val welcomeState = FakeWelcomeState() val analyticsService = FakeAnalyticsService() + val migrationScreenStore = InMemoryMigrationScreenStore() val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) - val state = createState(coroutineScope, welcomeState, analyticsService) + val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore) val steps = mutableListOf() - // First step, welcome screen + // First step, migration screen + steps.add(state.getNextStep(steps.lastOrNull())) + migrationScreenStore.setMigrationScreenShown(A_SESSION_ID) + + // Second step, welcome screen steps.add(state.getNextStep(steps.lastOrNull())) welcomeState.setWelcomeScreenShown() - // Second step, analytics opt in + // Third step, analytics opt in steps.add(state.getNextStep(steps.lastOrNull())) analyticsService.setDidAskUserConsent() @@ -80,6 +92,7 @@ class DefaultFtueStateTests { steps.add(state.getNextStep(steps.lastOrNull())) assertThat(steps).containsExactly( + FtueStep.MigrationScreen, FtueStep.WelcomeScreen, FtueStep.AnalyticsOptIn, null, // Final state @@ -93,7 +106,16 @@ class DefaultFtueStateTests { fun `if a check for a step is true, start from the next one`() = runTest { val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val analyticsService = FakeAnalyticsService() - val state = createState(coroutineScope = coroutineScope, analyticsService = analyticsService) + val migrationScreenStore = InMemoryMigrationScreenStore() + + val state = createState( + coroutineScope = coroutineScope, + analyticsService = analyticsService, + migrationScreenStore = migrationScreenStore, + ) + + migrationScreenStore.setMigrationScreenShown(A_SESSION_ID) + assertThat(state.getNextStep()).isEqualTo(FtueStep.WelcomeScreen) state.setWelcomeScreenShown() assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn) @@ -108,7 +130,14 @@ class DefaultFtueStateTests { private fun createState( coroutineScope: CoroutineScope, welcomeState: FakeWelcomeState = FakeWelcomeState(), - analyticsService: AnalyticsService = FakeAnalyticsService() - ) = DefaultFtueState(coroutineScope, analyticsService, welcomeState) - + analyticsService: AnalyticsService = FakeAnalyticsService(), + migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(), + matrixClient: MatrixClient = FakeMatrixClient(), + ) = DefaultFtueState( + coroutineScope = coroutineScope, + analyticsService = analyticsService, + welcomeScreenState = welcomeState, + migrationScreenStore = migrationScreenStore, + matrixClient = matrixClient, + ) } diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/migration/InMemoryMigrationScreenStore.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/migration/InMemoryMigrationScreenStore.kt new file mode 100644 index 0000000000..a77a4d001a --- /dev/null +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/migration/InMemoryMigrationScreenStore.kt @@ -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.ftue.impl.migration + +import io.element.android.libraries.matrix.api.core.SessionId + +class InMemoryMigrationScreenStore : MigrationScreenStore { + private val store = mutableMapOf() + + override fun isMigrationScreenNeeded(sessionId: SessionId): Boolean { + // If store does not have key return true, else return the opposite of the value + return store[sessionId]?.not() ?: true + } + + override fun setMigrationScreenShown(sessionId: SessionId) { + store[sessionId] = true + } + + override fun reset() { + store.clear() + } +} diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenPresenterTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenPresenterTest.kt new file mode 100644 index 0000000000..6e19879b86 --- /dev/null +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/migration/MigrationScreenPresenterTest.kt @@ -0,0 +1,69 @@ +/* + * 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.ftue.impl.migration + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MigrationScreenPresenterTest { + @Test + fun `present - initial`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isMigrating).isTrue() + } + } + + @Test + fun `present - migration end`() = runTest { + val matrixClient = FakeMatrixClient() + val migrationScreenStore = InMemoryMigrationScreenStore() + val presenter = createPresenter(matrixClient, migrationScreenStore) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isMigrating).isTrue() + assertThat(migrationScreenStore.isMigrationScreenNeeded(A_SESSION_ID)).isTrue() + // Simulate room list loaded + (matrixClient.roomListService as FakeRoomListService).postState(RoomListService.State.Running) + val nextState = awaitItem() + assertThat(nextState.isMigrating).isFalse() + assertThat(migrationScreenStore.isMigrationScreenNeeded(A_SESSION_ID)).isFalse() + } + } + + private fun createPresenter( + matrixClient: MatrixClient = FakeMatrixClient(), + migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(), + ) = MigrationScreenPresenter( + matrixClient, + migrationScreenStore, + ) +} diff --git a/features/invitelist/impl/build.gradle.kts b/features/invitelist/impl/build.gradle.kts index 3f8f1a44ed..cd008472b5 100644 --- a/features/invitelist/impl/build.gradle.kts +++ b/features/invitelist/impl/build.gradle.kts @@ -53,7 +53,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.push.test) testImplementation(projects.features.invitelist.test) - testImplementation(projects.features.analytics.test) + testImplementation(projects.services.analytics.test) ksp(libs.showkase.processor) } diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListEvents.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListEvents.kt index 0b8f03b45a..38055b7090 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListEvents.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListEvents.kt @@ -19,14 +19,12 @@ package io.element.android.features.invitelist.impl import io.element.android.features.invitelist.impl.model.InviteListInviteSummary sealed interface InviteListEvents { - data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents data class DeclineInvite(val invite: InviteListInviteSummary) : InviteListEvents - object ConfirmDeclineInvite: InviteListEvents - object CancelDeclineInvite: InviteListEvents - - object DismissAcceptError: InviteListEvents - object DismissDeclineError: InviteListEvents + data object ConfirmDeclineInvite: InviteListEvents + data object CancelDeclineInvite: InviteListEvents + data object DismissAcceptError: InviteListEvents + data object DismissDeclineError: InviteListEvents } diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt index 5a7761ebc0..c1e00727f9 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt @@ -32,6 +32,6 @@ data class InviteListState( ) sealed interface InviteDeclineConfirmationDialog { - object Hidden : InviteDeclineConfirmationDialog + data object Hidden : InviteDeclineConfirmationDialog data class Visible(val isDirect: Boolean, val name: String) : InviteDeclineConfirmationDialog } diff --git a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt b/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt index f3eef2784c..adb2042b62 100644 --- a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt +++ b/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt @@ -20,7 +20,6 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth -import io.element.android.features.analytics.test.FakeAnalyticsService import io.element.android.features.invitelist.api.SeenInvitesStore import io.element.android.features.invitelist.test.FakeSeenInvitesStore import io.element.android.libraries.architecture.Async @@ -44,6 +43,7 @@ import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analytics.test.FakeAnalyticsService import kotlinx.coroutines.test.runTest import org.junit.Test diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt index d1a3369ac6..9a9eb80997 100644 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt @@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.core.RoomId sealed interface LeaveRoomEvent { data class ShowConfirmation(val roomId: RoomId) : LeaveRoomEvent - object HideConfirmation : LeaveRoomEvent + data object HideConfirmation : LeaveRoomEvent data class LeaveRoom(val roomId: RoomId) : LeaveRoomEvent - object HideError : LeaveRoomEvent + data object HideError : LeaveRoomEvent } diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt index 7cb9926677..3f14833cf0 100644 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt @@ -25,19 +25,19 @@ data class LeaveRoomState( val eventSink: (LeaveRoomEvent) -> Unit = {}, ) { sealed interface Confirmation { - object Hidden : Confirmation + data object Hidden : Confirmation data class Generic(val roomId: RoomId) : Confirmation data class PrivateRoom(val roomId: RoomId) : Confirmation data class LastUserInRoom(val roomId: RoomId) : Confirmation } sealed interface Progress { - object Hidden : Progress - object Shown : Progress + data object Hidden : Progress + data object Shown : Progress } sealed interface Error { - object Hidden : Error - object Shown : Error + data object Hidden : Error + data object Shown : Error } } diff --git a/features/location/impl/build.gradle.kts b/features/location/impl/build.gradle.kts index e808eed11c..325003b110 100644 --- a/features/location/impl/build.gradle.kts +++ b/features/location/impl/build.gradle.kts @@ -55,6 +55,6 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(libs.test.truth) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.features.analytics.test) + testImplementation(projects.services.analytics.test) testImplementation(projects.features.messages.test) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvents.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvents.kt index fc18ec6ede..f4282bc59c 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvents.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvents.kt @@ -17,5 +17,5 @@ package io.element.android.features.location.impl.common.permissions sealed interface PermissionsEvents { - object RequestPermissions : PermissionsEvents + data object RequestPermissions : PermissionsEvents } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt index 76b786c638..d58361a82f 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt @@ -22,9 +22,9 @@ data class PermissionsState( val eventSink: (PermissionsEvents) -> Unit = {}, ) { sealed interface Permissions { - object AllGranted : Permissions - object SomeGranted : Permissions - object NoneGranted : Permissions + data object AllGranted : Permissions + data object SomeGranted : Permissions + data object NoneGranted : Permissions } val isAnyGranted: Boolean diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEvents.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEvents.kt index 2f0686da27..d39be47b40 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEvents.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEvents.kt @@ -30,13 +30,9 @@ sealed interface SendLocationEvents { ) } - object SwitchToMyLocationMode : SendLocationEvents - - object SwitchToPinLocationMode : SendLocationEvents - - object DismissDialog : SendLocationEvents - - object RequestPermissions : SendLocationEvents - - object OpenAppSettings : SendLocationEvents + data object SwitchToMyLocationMode : SendLocationEvents + data object SwitchToPinLocationMode : SendLocationEvents + data object DismissDialog : SendLocationEvents + data object RequestPermissions : SendLocationEvents + data object OpenAppSettings : SendLocationEvents } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt index 3aeec5f046..5dae23c998 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt @@ -24,13 +24,13 @@ data class SendLocationState( val eventSink: (SendLocationEvents) -> Unit = {}, ) { sealed interface Mode { - object SenderLocation : Mode - object PinLocation : Mode + data object SenderLocation : Mode + data object PinLocation : Mode } sealed interface Dialog { - object None : Dialog - object PermissionRationale : Dialog - object PermissionDenied : Dialog + data object None : Dialog + data object PermissionRationale : Dialog + data object PermissionDenied : Dialog } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvents.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvents.kt index f6c3c12528..21eed4a22d 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvents.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvents.kt @@ -17,11 +17,9 @@ package io.element.android.features.location.impl.show sealed interface ShowLocationEvents { - object Share : ShowLocationEvents + data object Share : ShowLocationEvents data class TrackMyLocation(val enabled: Boolean) : ShowLocationEvents - object DismissDialog : ShowLocationEvents - - object RequestPermissions : ShowLocationEvents - - object OpenAppSettings : ShowLocationEvents + data object DismissDialog : ShowLocationEvents + data object RequestPermissions : ShowLocationEvents + data object OpenAppSettings : ShowLocationEvents } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt index 0be6938ef6..67bcfa382e 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt @@ -28,8 +28,8 @@ data class ShowLocationState( val eventSink: (ShowLocationEvents) -> Unit, ) { sealed interface Dialog { - object None : Dialog - object PermissionRationale : Dialog - object PermissionDenied : Dialog + data object None : Dialog + data object PermissionRationale : Dialog + data object PermissionDenied : Dialog } } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt index 34962081e2..21766a2cf0 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt @@ -21,7 +21,6 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth import im.vector.app.features.analytics.plan.Composer -import io.element.android.features.analytics.test.FakeAnalyticsService import io.element.android.features.location.api.Location import io.element.android.features.location.impl.common.actions.FakeLocationActions import io.element.android.features.location.impl.common.permissions.PermissionsEvents @@ -34,6 +33,7 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.SendLocationInvocation import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.services.analytics.test.FakeAnalyticsService import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import org.junit.Test diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt index 6e90a390c4..6d87872879 100644 --- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt +++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt @@ -17,6 +17,6 @@ package io.element.android.features.login.api.oidc sealed interface OidcAction { - object GoBack : OidcAction + data object GoBack : OidcAction data class Success(val url: String) : OidcAction } diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 91b8a2f543..ae13197f05 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -19,7 +19,7 @@ plugins { alias(libs.plugins.anvil) alias(libs.plugins.ksp) id("kotlin-parcelize") - kotlin("plugin.serialization") version "1.9.0" + kotlin("plugin.serialization") version "1.9.10" } android { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index fe47fb1b67..59c85bedf8 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -22,7 +22,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin @@ -33,6 +35,8 @@ import com.bumble.appyx.navmodel.backstack.operation.singleTop import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.login.api.oidc.OidcAction +import io.element.android.features.login.api.oidc.OidcActionFlow import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler @@ -51,6 +55,8 @@ import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.theme.ElementTheme +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @ContributesNode(AppScope::class) @@ -61,6 +67,7 @@ class LoginFlowNode @AssistedInject constructor( private val customTabHandler: CustomTabHandler, private val accountProviderDataSource: AccountProviderDataSource, private val defaultLoginUserStory: DefaultLoginUserStory, + private val oidcActionFlow: OidcActionFlow, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.ConfirmAccountProvider, @@ -78,23 +85,40 @@ class LoginFlowNode @AssistedInject constructor( private val inputs: Inputs = inputs() + private var customChromeTabStarted = false + override fun onBuilt() { super.onBuilt() defaultLoginUserStory.setLoginFlowIsDone(false) + lifecycle.subscribe( + onResume = { + if (customChromeTabStarted) { + customChromeTabStarted = false + // Workaround to detect that the Custom Chrome Tab has been closed + // If there is no coming OidcAction (that would end this Node), + // consider that the user has cancelled the login + // by pressing back or by closing the Custom Chrome Tab. + lifecycleScope.launch { + delay(5000) + oidcActionFlow.post(OidcAction.GoBack) + } + } + } + ) } sealed interface NavTarget : Parcelable { @Parcelize - object ConfirmAccountProvider : NavTarget + data object ConfirmAccountProvider : NavTarget @Parcelize - object ChangeAccountProvider : NavTarget + data object ChangeAccountProvider : NavTarget @Parcelize - object SearchAccountProvider : NavTarget + data object SearchAccountProvider : NavTarget @Parcelize - object LoginPassword : NavTarget + data object LoginPassword : NavTarget @Parcelize data class WaitList(val loginFormState: LoginFormState) : NavTarget @@ -113,7 +137,10 @@ class LoginFlowNode @AssistedInject constructor( override fun onOidcDetails(oidcDetails: OidcDetails) { if (customTabAvailabilityChecker.supportCustomTab()) { // In this case open a Chrome Custom tab - activity?.let { customTabHandler.open(it, darkTheme, oidcDetails.url) } + activity?.let { + customChromeTabStarted = true + customTabHandler.open(it, darkTheme, oidcDetails.url) + } } else { // Fallback to WebView mode backstack.push(NavTarget.OidcView(oidcDetails)) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt index cd1cb7b4ce..3a1945da9d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt @@ -20,5 +20,5 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider sealed interface ChangeServerEvents { data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents - object ClearError : ChangeServerEvents + data object ClearError : ChangeServerEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt index 444ea3d3f2..f9289898b6 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt @@ -27,7 +27,7 @@ sealed class ChangeServerError : Throwable() { @Composable fun message(): String = stringResource(messageId) } - object SlidingSyncAlert : ChangeServerError() + data object SlidingSyncAlert : ChangeServerError() companion object { fun from(error: Throwable): ChangeServerError = when (error) { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt index 6265cfc85a..ae0a912ba6 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt @@ -19,7 +19,7 @@ package io.element.android.features.login.impl.oidc.webview import io.element.android.features.login.api.oidc.OidcAction sealed interface OidcEvents { - object Cancel : OidcEvents + data object Cancel : OidcEvents data class OidcActionEvent(val oidcAction: OidcAction): OidcEvents - object ClearError : OidcEvents + data object ClearError : OidcEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt index 1ba3cc3028..6003c0a716 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt @@ -17,6 +17,6 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider sealed interface ConfirmAccountProviderEvents { - object Continue : ConfirmAccountProviderEvents - object ClearError : ConfirmAccountProviderEvents + data object Continue : ConfirmAccountProviderEvents + data object ClearError : ConfirmAccountProviderEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index 61ef4d724b..d5412cb139 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -68,9 +68,9 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( } LaunchedEffect(Unit) { - launch { - defaultOidcActionFlow.collect { - onOidcAction(it, loginFlowAction) + defaultOidcActionFlow.collect { oidcAction -> + if (oidcAction != null) { + onOidcAction(oidcAction, loginFlowAction) } } } @@ -113,10 +113,9 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( } private suspend fun onOidcAction( - oidcAction: OidcAction?, + oidcAction: OidcAction, loginFlowAction: MutableState>, ) { - oidcAction ?: return loginFlowAction.value = Async.Loading() when (oidcAction) { OidcAction.GoBack -> { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt index a870b88c58..c2c98101a5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt @@ -31,6 +31,6 @@ data class ConfirmAccountProviderState( } sealed interface LoginFlow { - object PasswordLogin : LoginFlow + data object PasswordLogin : LoginFlow data class OidcFlow(val oidcDetails: OidcDetails) : LoginFlow } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt index e6f23ca418..818fb97860 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt @@ -19,6 +19,6 @@ package io.element.android.features.login.impl.screens.loginpassword sealed interface LoginPasswordEvents { data class SetLogin(val login: String) : LoginPasswordEvents data class SetPassword(val password: String) : LoginPasswordEvents - object Submit : LoginPasswordEvents - object ClearError : LoginPasswordEvents + data object Submit : LoginPasswordEvents + data object ClearError : LoginPasswordEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListEvents.kt index 5ceee99f91..d5722e66d6 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListEvents.kt @@ -17,7 +17,7 @@ package io.element.android.features.login.impl.screens.waitlistscreen sealed interface WaitListEvents { - object AttemptLogin : WaitListEvents - object ClearError : WaitListEvents - object Continue : WaitListEvents + data object AttemptLogin : WaitListEvents + data object ClearError : WaitListEvents + data object Continue : WaitListEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt index 1bce4b39a9..5ac53e851e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt @@ -16,32 +16,17 @@ package io.element.android.features.login.impl.screens.waitlistscreen -import androidx.annotation.StringRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment -import androidx.compose.ui.BiasAbsoluteAlignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -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 @@ -50,15 +35,13 @@ import io.element.android.features.login.impl.R import io.element.android.features.login.impl.error.isWaitListError import io.element.android.features.login.impl.error.loginError import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.atomic.pages.SunsetPage import io.element.android.libraries.designsystem.components.dialogs.RetryDialog 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.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.utils.OnLifecycleEvent -import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings // Ref: https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=6761-148425 @@ -75,12 +58,7 @@ fun WaitListView( else -> Unit } } - - Box(modifier = modifier) { - WaitListBackground() - WaitListContent(state, onCancelClicked) - WaitListError(state) - } + WaitListContent(state, onCancelClicked, modifier) } @Composable @@ -101,136 +79,69 @@ private fun WaitListError(state: WaitListState) { } } -@Composable -private fun WaitListBackground( - modifier: Modifier = Modifier, -) { - Column(modifier = modifier.fillMaxSize()) { - Box( - modifier = Modifier - .fillMaxWidth() - .weight(0.3f) - .background(Color.White) - ) - Image( - modifier = Modifier - .fillMaxWidth(), - painter = painterResource(id = R.drawable.light_dark), - contentScale = ContentScale.Crop, - contentDescription = null, - ) - Box( - modifier = Modifier - .fillMaxWidth() - .weight(0.7f) - .background(Color(0xFF121418)) - ) - } -} - @Composable private fun WaitListContent( state: WaitListState, onCancelClicked: () -> Unit, modifier: Modifier = Modifier, ) { - ElementTheme( - darkTheme = true + Box( + modifier = modifier.fillMaxSize(), ) { - Box( - modifier = modifier - .fillMaxSize() - .systemBarsPadding() - .padding(horizontal = 16.dp, vertical = 16.dp) - ) { - if (state.loginAction !is Async.Success) { - CompositionLocalProvider(LocalContentColor provides Color.Black) { - TextButton( - text = stringResource(CommonStrings.action_cancel), - onClick = onCancelClicked, - ) - } - } - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = BiasAbsoluteAlignment( - horizontalBias = 0f, - verticalBias = -0.05f - ) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (state.loginAction.isLoading()) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - strokeWidth = 2.dp, - color = ElementTheme.colors.iconPrimary - ) - } else { - Spacer(modifier = Modifier.height(24.dp)) - } - Spacer(modifier = Modifier.height(18.dp)) - val titleRes = when (state.loginAction) { - is Async.Success -> R.string.screen_waitlist_title_success - else -> R.string.screen_waitlist_title - } - Text( - text = withColoredPeriod(titleRes), - style = ElementTheme.typography.fontHeadingXlBold, - textAlign = TextAlign.Center, - color = ElementTheme.colors.textPrimary, - ) - Spacer(modifier = Modifier.height(8.dp)) - val subtitle = when (state.loginAction) { - is Async.Success -> stringResource( - id = R.string.screen_waitlist_message_success, - state.appName, - ) - else -> stringResource( - id = R.string.screen_waitlist_message, - state.appName, - state.serverName, - ) - } - Text( - modifier = Modifier.widthIn(max = 360.dp), - text = subtitle, - style = ElementTheme.typography.fontBodyLgRegular, - textAlign = TextAlign.Center, - color = ElementTheme.colors.textPrimary, - ) - } - } - if (state.loginAction is Async.Success) { - Button( - text = stringResource(id = CommonStrings.action_continue), - onClick = { state.eventSink.invoke(WaitListEvents.Continue) }, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(bottom = 8.dp), - ) + val title = stringResource( + when (state.loginAction) { + is Async.Success -> R.string.screen_waitlist_title_success + else -> R.string.screen_waitlist_title } + ) + val subtitle = when (state.loginAction) { + is Async.Success -> stringResource( + id = R.string.screen_waitlist_message_success, + state.appName, + ) + else -> stringResource( + id = R.string.screen_waitlist_message, + state.appName, + state.serverName, + ) } + SunsetPage( + isLoading = state.loginAction.isLoading(), + title = title, + subtitle = subtitle, + ) { + OverallContent(state, onCancelClicked) + } + WaitListError(state) } } @Composable -private fun withColoredPeriod( - @StringRes textRes: Int, -) = buildAnnotatedString { - val text = stringResource(textRes) - append(text) - if (text.endsWith(".")) { - addStyle( - style = SpanStyle( - // Light.colorGreen700 - color = Color(0xff0bc491), - ), - start = text.length - 1, - end = text.length, - ) +private fun OverallContent( + state: WaitListState, + onCancelClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier.fillMaxSize()) { + if (state.loginAction !is Async.Success) { + CompositionLocalProvider(LocalContentColor provides Color.Black) { + TextButton( + text = stringResource(CommonStrings.action_cancel), + onClick = onCancelClicked, + ) + } + } + if (state.loginAction is Async.Success) { + Button( + text = stringResource(id = CommonStrings.action_continue), + onClick = { state.eventSink.invoke(WaitListEvents.Continue) }, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 8.dp), + ) + } + } } diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml index 02581401bf..5d697bf34a 100644 --- a/features/login/impl/src/main/res/values-cs/translations.xml +++ b/features/login/impl/src/main/res/values-cs/translations.xml @@ -9,7 +9,7 @@ "Chystáte se přihlásit do %s" "Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily." "Chystáte se vytvořit účet na %s" - "Matrix.org je otevřená síť pro bezpečnou, decentralizovanou komunikaci." + "Matrix.org je velký bezplatný server ve veřejné síti Matrix pro bezpečnou decentralizovanou komunikaci, který provozuje nadace Matrix.org." "Jiný" "Použijte jiného poskytovatele účtu, například vlastní soukromý server nebo pracovní účet." "Změnit poskytovatele účtu" diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml index 099d00e83b..7464e17f8d 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -9,7 +9,7 @@ "Du bist dabei dich bei %s anzumelden" "Hier werden deine Konversationen stattfinden — genauso wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren." "Du bist dabei ein Konto auf %s zu erstellen" - "Matrix.org ist ein offenes Netzwerk für sichere, dezentralisierte Kommunikation." + "Matrix.org ist ein großer, kostenloser Server im öffentlichen Matrix-Netzwerk für sichere, dezentrale Kommunikation, der von der Matrix.org Foundation betrieben wird." "Andere" "Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Arbeitskonto." "Kontoanbieter ändern" diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml index 77d171bdf6..74f23cc5c9 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -9,7 +9,6 @@ "Vous êtes sur le point de vous connecter à %s" "C\'est ici que vos conversations seront stockées - tout comme vous utiliseriez un fournisseur de messagerie pour conserver vos e-mails." "Vous êtes sur le point de créer un compte sur %s" - "Matrix.org est un réseau ouvert pour des communications sécurisées et décentralisées." "Autre" "Utilisez un autre fournisseur de compte, tel que votre propre serveur ou un compte professionnel." "Changer de fournisseur" diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml index 46596f2b95..0c58c45d03 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -9,7 +9,6 @@ "Sunteți pe cale să vă conectați la %s" "Aici vor trăi conversațiile - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile." "Sunteți pe cale să creați un cont pe %s" - "Matrix.org este o rețea deschisă pentru o comunicare sigură și descentralizată." "Altul" "Utilizați un alt furnizor de cont, cum ar fi propriul server privat sau un cont de serviciu." "Schimbați furnizorul contului" diff --git a/features/login/impl/src/main/res/values-ru/translations.xml b/features/login/impl/src/main/res/values-ru/translations.xml index aa8b459f59..242ec4bb2c 100644 --- a/features/login/impl/src/main/res/values-ru/translations.xml +++ b/features/login/impl/src/main/res/values-ru/translations.xml @@ -9,7 +9,6 @@ "Вы собираетесь войти в %s" "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем." "Вы собираетесь создать учетную запись на %s" - "Matrix.org — это открытая сеть для безопасной децентрализованной связи." "Другое" "Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись." "Сменить поставщика учетной записи" diff --git a/features/login/impl/src/main/res/values-sk/translations.xml b/features/login/impl/src/main/res/values-sk/translations.xml index a48fed0fd5..14a5407055 100644 --- a/features/login/impl/src/main/res/values-sk/translations.xml +++ b/features/login/impl/src/main/res/values-sk/translations.xml @@ -9,7 +9,6 @@ "Chystáte sa prihlásiť do %s" "Tu budú žiť vaše konverzácie — podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov." "Chystáte sa vytvoriť účet na %s" - "Matrix.org je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu." "Iný" "Použite iného poskytovateľa účtu, ako napríklad vlastný súkromný server alebo pracovný účet." "Zmeniť poskytovateľa účtu" diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 236de68212..e09f4fe693 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -9,7 +9,7 @@ "You’re about to sign in to %s" "This is where your conversations will live — just like you would use an email provider to keep your emails." "You’re about to create an account on %s" - "Matrix.org is an open network for secure, decentralized communication." + "Matrix.org is a large, free server on the public Matrix network for secure, decentralised communication, run by the Matrix.org Foundation." "Other" "Use a different account provider, such as your own private server or a work account." "Change account provider" diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceEvents.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceEvents.kt index 2dad1623ab..50dad213fd 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceEvents.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceEvents.kt @@ -17,5 +17,5 @@ package io.element.android.features.logout.api sealed interface LogoutPreferenceEvents { - object Logout : LogoutPreferenceEvents + data object Logout : LogoutPreferenceEvents } diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceScreen.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceScreen.kt index f9f23ac9c4..7be1c977f2 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceScreen.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceScreen.kt @@ -34,7 +34,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight @Composable fun LogoutPreferenceView( state: LogoutPreferenceState, - onSuccessLogout: (String?) -> Unit = {} + onSuccessLogout: (logoutUrlResult: String?) -> Unit ) { val eventSink = state.eventSink if (state.logoutAction is Async.Success) { @@ -96,5 +96,8 @@ internal fun LogoutPreferenceViewDarkPreview() = ElementPreviewDark { ContentToP @Composable private fun ContentToPreview() { - LogoutPreferenceView(aLogoutPreferenceState()) + LogoutPreferenceView( + aLogoutPreferenceState(), + onSuccessLogout = {} + ) } diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 4746cff1de..71030c3ca0 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -71,7 +71,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.features.networkmonitor.test) - testImplementation(projects.features.analytics.test) + testImplementation(projects.services.analytics.test) testImplementation(projects.tests.testutils) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.mediaupload.test) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt index d475b5bc8c..b901f7e130 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt @@ -24,7 +24,7 @@ sealed interface MessagesEvents { data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents data class ToggleReaction(val emoji: String, val eventId: EventId) : MessagesEvents data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents - object Dismiss : MessagesEvents + data object Dismiss : MessagesEvents } enum class InviteDialogAction { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index da10171d0a..fcb2e7e5e8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -46,6 +46,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -64,6 +65,7 @@ class MessagesFlowNode @AssistedInject constructor( @Assisted plugins: List, private val sendLocationEntryPoint: SendLocationEntryPoint, private val showLocationEntryPoint: ShowLocationEntryPoint, + private val createPollEntryPoint: CreatePollEntryPoint, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.Messages, @@ -75,7 +77,7 @@ class MessagesFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - object Messages : NavTarget + data object Messages : NavTarget @Parcelize data class MediaViewer( @@ -100,7 +102,10 @@ class MessagesFlowNode @AssistedInject constructor( data class ReportMessage(val eventId: EventId, val senderId: UserId) : NavTarget @Parcelize - object SendLocation : NavTarget + data object SendLocation : NavTarget + + @Parcelize + data object CreatePoll : NavTarget } private val callback = plugins().firstOrNull() @@ -140,6 +145,10 @@ class MessagesFlowNode @AssistedInject constructor( override fun onSendLocationClicked() { backstack.push(NavTarget.SendLocation) } + + override fun onCreatePollClicked() { + backstack.push(NavTarget.CreatePoll) + } } createNode(buildContext, listOf(callback)) } @@ -179,6 +188,9 @@ class MessagesFlowNode @AssistedInject constructor( NavTarget.SendLocation -> { sendLocationEntryPoint.createNode(this, buildContext) } + NavTarget.CreatePoll -> { + createPollEntryPoint.createNode(this, buildContext) + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 3f201a8e4c..6a3cf502d7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -58,6 +58,7 @@ class MessagesNode @AssistedInject constructor( fun onForwardEventClicked(eventId: EventId) fun onReportMessage(eventId: EventId, senderId: UserId) fun onSendLocationClicked() + fun onCreatePollClicked() } init { @@ -99,6 +100,10 @@ class MessagesNode @AssistedInject constructor( callback?.onSendLocationClicked() } + private fun onCreatePollClicked() { + callback?.onCreatePollClicked() + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -110,6 +115,7 @@ class MessagesNode @AssistedInject constructor( onPreviewAttachments = this::onPreviewAttachments, onUserDataClicked = this::onUserDataClicked, onSendLocationClicked = this::onSendLocationClicked, + onCreatePollClicked = this::onCreatePollClicked, modifier = modifier, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 145813fe54..92b15f4fc0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -97,6 +97,7 @@ fun MessagesView( onUserDataClicked: (UserId) -> Unit, onPreviewAttachments: (ImmutableList) -> Unit, onSendLocationClicked: () -> Unit, + onCreatePollClicked: () -> Unit, modifier: Modifier = Modifier, ) { LogCompositions(tag = "MessagesScreen", msg = "Root") @@ -175,6 +176,7 @@ fun MessagesView( onReactionLongClicked = ::onEmojiReactionLongClicked, onMoreReactionsClicked = ::onMoreReactionsClicked, onSendLocationClicked = onSendLocationClicked, + onCreatePollClicked = onCreatePollClicked, onSwipeToReply = { targetEvent -> state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent)) }, @@ -267,6 +269,7 @@ private fun MessagesViewContent( onMessageLongClicked: (TimelineItem.Event) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit, onSendLocationClicked: () -> Unit, + onCreatePollClicked: () -> Unit, modifier: Modifier = Modifier, onSwipeToReply: (TimelineItem.Event) -> Unit, ) { @@ -295,6 +298,7 @@ private fun MessagesViewContent( MessageComposerView( state = state.composerState, onSendLocationClicked = onSendLocationClicked, + onCreatePollClicked = onCreatePollClicked, modifier = Modifier .fillMaxWidth() .wrapContentHeight(Alignment.Bottom) @@ -401,5 +405,6 @@ private fun ContentToPreview(state: MessagesState) { onPreviewAttachments = {}, onUserDataClicked = {}, onSendLocationClicked = {}, + onCreatePollClicked = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt index 3c796036e7..c5e6618736 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt @@ -19,6 +19,6 @@ package io.element.android.features.messages.impl.actionlist import io.element.android.features.messages.impl.timeline.model.TimelineItem sealed interface ActionListEvents { - object Clear : ActionListEvents + data object Clear : ActionListEvents data class ComputeForMessage(val event: TimelineItem.Event, val canRedact: Boolean) : ActionListEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index f71c750c22..e654365bcd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.canBeCopied @@ -96,6 +97,22 @@ class ActionListPresenter @Inject constructor( } } } + is TimelineItemPollContent -> { + buildList { + if (timelineItem.content.canBeCopied()) { + add(TimelineItemAction.Copy) + } + if (buildMeta.isDebuggable) { + add(TimelineItemAction.Developer) + } + if (!timelineItem.isMine) { + add(TimelineItemAction.ReportContent) + } + if (timelineItem.isMine || userCanRedact) { + add(TimelineItemAction.Redact) + } + } + } else -> buildList { if (timelineItem.isRemote) { // Can only reply or forward messages already uploaded to the server diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt index aac3469218..a8fbf81486 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt @@ -28,7 +28,7 @@ data class ActionListState( val eventSink: (ActionListEvents) -> Unit, ) { sealed interface Target { - object None : Target + data object None : Target data class Loading(val event: TimelineItem.Event) : Target data class Success( val event: TimelineItem.Event, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index 8b2922e1d6..b6141218eb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -28,11 +28,11 @@ sealed class TimelineItemAction( @DrawableRes val icon: Int, val destructive: Boolean = false ) { - object Forward : TimelineItemAction(CommonStrings.action_forward, VectorIcons.Forward) - object Copy : TimelineItemAction(CommonStrings.action_copy, VectorIcons.Copy) - object Redact : TimelineItemAction(CommonStrings.action_remove, VectorIcons.Delete, destructive = true) - object Reply : TimelineItemAction(CommonStrings.action_reply, VectorIcons.Reply) - object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit) - object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode) - object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true) + data object Forward : TimelineItemAction(CommonStrings.action_forward, VectorIcons.Forward) + data object Copy : TimelineItemAction(CommonStrings.action_copy, VectorIcons.Copy) + data object Redact : TimelineItemAction(CommonStrings.action_remove, VectorIcons.Delete, destructive = true) + data object Reply : TimelineItemAction(CommonStrings.action_reply, VectorIcons.Reply) + data object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit) + data object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode) + data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvents.kt index 14a6a3fb2d..6ce9348fcb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvents.kt @@ -20,6 +20,6 @@ import androidx.compose.runtime.Immutable @Immutable sealed interface AttachmentsPreviewEvents { - object SendAttachment : AttachmentsPreviewEvents - object ClearSendState : AttachmentsPreviewEvents + data object SendAttachment : AttachmentsPreviewEvents + data object ClearSendState : AttachmentsPreviewEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt index e41f43040f..183e1ea590 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt @@ -25,13 +25,13 @@ data class AttachmentsPreviewState( ) sealed interface SendActionState { - object Idle : SendActionState + data object Idle : SendActionState sealed interface Sending : SendActionState { - object Processing : Sending + data object Processing : Sending data class Uploading(val progress: Float) : Sending } data class Failure(val error: Throwable) : SendActionState - object Done : SendActionState + data object Done : SendActionState } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt index 0ae406efff..f7058e95b3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt @@ -21,9 +21,9 @@ import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails sealed interface ForwardMessagesEvents { data class SetSelectedRoom(val room: RoomSummaryDetails) : ForwardMessagesEvents // TODO remove to restore multi-selection - object RemoveSelectedRoom : ForwardMessagesEvents - object ToggleSearchActive : ForwardMessagesEvents + data object RemoveSelectedRoom : ForwardMessagesEvents + data object ToggleSearchActive : ForwardMessagesEvents data class UpdateQuery(val query: String) : ForwardMessagesEvents - object ForwardEvent : ForwardMessagesEvents - object ClearError : ForwardMessagesEvents + data object ForwardEvent : ForwardMessagesEvents + data object ClearError : ForwardMessagesEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt index b680ee58c9..a3d9632f18 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt @@ -17,9 +17,9 @@ package io.element.android.features.messages.impl.media.viewer sealed interface MediaViewerEvents { - object SaveOnDisk: MediaViewerEvents - object Share: MediaViewerEvents - object OpenWith: MediaViewerEvents - object RetryLoading : MediaViewerEvents - object ClearLoadingError : MediaViewerEvents + data object SaveOnDisk: MediaViewerEvents + data object Share: MediaViewerEvents + data object OpenWith: MediaViewerEvents + data object RetryLoading : MediaViewerEvents + data object ClearLoadingError : MediaViewerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt index b554ef98f4..43805bb5c0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt @@ -24,6 +24,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ListItem import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AttachFile +import androidx.compose.material.icons.filled.BarChart import androidx.compose.material.icons.filled.Collections import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.PhotoCamera @@ -52,6 +53,7 @@ import io.element.android.libraries.designsystem.theme.components.Text internal fun AttachmentsBottomSheet( state: MessageComposerState, onSendLocationClicked: () -> Unit, + onCreatePollClicked: () -> Unit, modifier: Modifier = Modifier, ) { val localView = LocalView.current @@ -85,6 +87,7 @@ internal fun AttachmentsBottomSheet( AttachmentSourcePickerMenu( state = state, onSendLocationClicked = onSendLocationClicked, + onCreatePollClicked = onCreatePollClicked, ) } } @@ -95,6 +98,7 @@ internal fun AttachmentsBottomSheet( internal fun AttachmentSourcePickerMenu( state: MessageComposerState, onSendLocationClicked: () -> Unit, + onCreatePollClicked: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -131,6 +135,16 @@ internal fun AttachmentSourcePickerMenu( text = { Text(stringResource(R.string.screen_room_attachment_source_location)) }, ) } + if (state.canCreatePoll) { + ListItem( + modifier = Modifier.clickable { + state.eventSink(MessageComposerEvents.PickAttachmentSource.Poll) + onCreatePollClicked() + }, + icon = { Icon(Icons.Default.BarChart, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) }, + ) + } } } @@ -142,5 +156,6 @@ internal fun AttachmentSourcePickerMenuPreview() = ElementPreview { canShareLocation = true, ), onSendLocationClicked = {}, + onCreatePollClicked = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index d040c503b1..d99eb3c158 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -21,20 +21,21 @@ import io.element.android.libraries.textcomposer.MessageComposerMode @Immutable sealed interface MessageComposerEvents { - object ToggleFullScreenState : MessageComposerEvents + data object ToggleFullScreenState : MessageComposerEvents data class FocusChanged(val hasFocus: Boolean) : MessageComposerEvents data class SendMessage(val message: String) : MessageComposerEvents - object CloseSpecialMode : MessageComposerEvents + data object CloseSpecialMode : MessageComposerEvents data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents data class UpdateText(val text: String) : MessageComposerEvents - object AddAttachment : MessageComposerEvents - object DismissAttachmentMenu : MessageComposerEvents + data object AddAttachment : MessageComposerEvents + data object DismissAttachmentMenu : MessageComposerEvents sealed interface PickAttachmentSource : MessageComposerEvents { - object FromGallery : PickAttachmentSource - object FromFiles : PickAttachmentSource - object PhotoFromCamera : PickAttachmentSource - object VideoFromCamera : PickAttachmentSource - object Location : PickAttachmentSource + data object FromGallery : PickAttachmentSource + data object FromFiles : PickAttachmentSource + data object PhotoFromCamera : PickAttachmentSource + data object VideoFromCamera : PickAttachmentSource + data object Location : PickAttachmentSource + data object Poll : PickAttachmentSource } - object CancelSendAttachment : MessageComposerEvents + data object CancelSendAttachment : MessageComposerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 5477b10c63..cc735dc008 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -83,6 +83,11 @@ class MessageComposerPresenter @Inject constructor( canShareLocation.value = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing) } + val canCreatePoll = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + canCreatePoll.value = featureFlagService.isFeatureEnabled(FeatureFlags.Polls) + } + val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType -> handlePickedMedia(attachmentsState, uri, mimeType) } @@ -179,6 +184,10 @@ class MessageComposerPresenter @Inject constructor( showAttachmentSourcePicker = false // Navigation to the location picker screen is done at the view layer } + MessageComposerEvents.PickAttachmentSource.Poll -> { + showAttachmentSourcePicker = false + // Navigation to the create poll screen is done at the view layer + } is MessageComposerEvents.CancelSendAttachment -> { ongoingSendAttachmentJob.value?.let { it.cancel() @@ -195,6 +204,7 @@ class MessageComposerPresenter @Inject constructor( mode = messageComposerContext.composerMode, showAttachmentSourcePicker = showAttachmentSourcePicker, canShareLocation = canShareLocation.value, + canCreatePoll = canCreatePoll.value, attachmentsState = attachmentsState.value, eventSink = ::handleEvents ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index 32faaf9d81..dbbc62ca47 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -29,6 +29,7 @@ data class MessageComposerState( val mode: MessageComposerMode, val showAttachmentSourcePicker: Boolean, val canShareLocation: Boolean, + val canCreatePoll: Boolean, val attachmentsState: AttachmentsState, val eventSink: (MessageComposerEvents) -> Unit ) { @@ -37,7 +38,7 @@ data class MessageComposerState( @Immutable sealed interface AttachmentsState { - object None : AttachmentsState + data object None : AttachmentsState data class Previewing(val attachments: ImmutableList) : AttachmentsState sealed interface Sending : AttachmentsState { data class Processing(val attachments: ImmutableList) : Sending diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index a1fbb7ffa0..2217b574b4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -33,6 +33,7 @@ fun aMessageComposerState( mode: MessageComposerMode = MessageComposerMode.Normal(content = ""), showAttachmentSourcePicker: Boolean = false, canShareLocation: Boolean = true, + canCreatePoll: Boolean = true, attachmentsState: AttachmentsState = AttachmentsState.None, ) = MessageComposerState( text = text, @@ -41,6 +42,7 @@ fun aMessageComposerState( mode = mode, showAttachmentSourcePicker = showAttachmentSourcePicker, canShareLocation = canShareLocation, + canCreatePoll = canCreatePoll, attachmentsState = attachmentsState, eventSink = {}, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 844635cef7..fd5421988b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.textcomposer.TextComposer fun MessageComposerView( state: MessageComposerState, onSendLocationClicked: () -> Unit, + onCreatePollClicked: () -> Unit, modifier: Modifier = Modifier, ) { fun onFullscreenToggle() { @@ -59,6 +60,7 @@ fun MessageComposerView( AttachmentsBottomSheet( state = state, onSendLocationClicked = onSendLocationClicked, + onCreatePollClicked = onCreatePollClicked, ) TextComposer( @@ -88,6 +90,7 @@ internal fun MessageComposerViewDarkPreview(@PreviewParameter(MessageComposerSta private fun ContentToPreview(state: MessageComposerState) { MessageComposerView( state = state, - onSendLocationClicked = {} + onSendLocationClicked = {}, + onCreatePollClicked = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageEvents.kt index ed5ee029e7..32007c0206 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageEvents.kt @@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.report sealed interface ReportMessageEvents { data class UpdateReason(val reason: String) : ReportMessageEvents - object ToggleBlockUser : ReportMessageEvents - object Report : ReportMessageEvents - object ClearError : ReportMessageEvents + data object ToggleBlockUser : ReportMessageEvents + data object Report : ReportMessageEvents + data object ClearError : ReportMessageEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt index 2bfed45470..30f9aade79 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt @@ -19,7 +19,7 @@ package io.element.android.features.messages.impl.timeline import io.element.android.libraries.matrix.api.core.EventId sealed interface TimelineEvents { - object LoadMore : TimelineEvents + data object LoadMore : TimelineEvents data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents data class OnScrollFinished(val firstIndex: Int) : TimelineEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index fbeb0af1d7..dd5a2df2e0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -16,6 +16,7 @@ package io.element.android.features.messages.impl.timeline.components +import android.annotation.SuppressLint import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -68,9 +69,11 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.designsystem.components.EqualWidthColumn import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -357,11 +360,15 @@ private fun MessageEventBubbleContent( onMessageLongClick: () -> Unit, inReplyToClick: () -> Unit, onTimestampClicked: () -> Unit, - modifier: Modifier = Modifier + @SuppressLint("ModifierParameter") bubbleModifier: Modifier = Modifier, // need to rename this modifier to distinguish it from the following ones ) { - val isMediaItem = event.content is TimelineItemImageContent - || event.content is TimelineItemVideoContent - || event.content is TimelineItemLocationContent + val timestampPosition = when (event.content) { + is TimelineItemImageContent, + is TimelineItemVideoContent, + is TimelineItemLocationContent -> TimestampPosition.Overlay + is TimelineItemPollContent -> TimestampPosition.Below + else -> TimestampPosition.Default + } val replyToDetails = event.inReplyTo as? InReplyTo.Ready // Long clicks are not not automatically propagated from a `clickable` @@ -384,96 +391,97 @@ private fun MessageEventBubbleContent( @Composable fun ContentAndTimestampView( - overlayTimestamp: Boolean, + timestampPosition: TimestampPosition, modifier: Modifier = Modifier, contentModifier: Modifier = Modifier, timestampModifier: Modifier = Modifier, ) { - if (overlayTimestamp) { - Box(modifier) { - ContentView(modifier = contentModifier) - TimelineEventTimestampView( - event = event, - onClick = onTimestampClicked, - onLongClick = ::onTimestampLongClick, - modifier = timestampModifier - .padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding - .background(ElementTheme.colors.bgSubtleSecondary, RoundedCornerShape(10.0.dp)) - .align(Alignment.BottomEnd) - .padding(horizontal = 4.dp, vertical = 2.dp) // Inner padding - ) - } - } else { - Box(modifier) { - ContentView(modifier = contentModifier) - TimelineEventTimestampView( - event = event, - onClick = onTimestampClicked, - onLongClick = ::onTimestampLongClick, - modifier = timestampModifier - .align(Alignment.BottomEnd) - .padding(horizontal = 8.dp, vertical = 4.dp) - ) - } + when (timestampPosition) { + TimestampPosition.Overlay -> + Box(modifier) { + ContentView(modifier = contentModifier) + TimelineEventTimestampView( + event = event, + onClick = onTimestampClicked, + onLongClick = ::onTimestampLongClick, + modifier = timestampModifier + .padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding + .background(ElementTheme.colors.bgSubtleSecondary, RoundedCornerShape(10.0.dp)) + .align(Alignment.BottomEnd) + .padding(horizontal = 4.dp, vertical = 2.dp) // Inner padding + ) + } + TimestampPosition.Aligned -> + Box(modifier) { + ContentView(modifier = contentModifier) + TimelineEventTimestampView( + event = event, + onClick = onTimestampClicked, + onLongClick = ::onTimestampLongClick, + modifier = timestampModifier + .align(Alignment.BottomEnd) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + TimestampPosition.Below -> + Column(modifier) { + ContentView(modifier = contentModifier) + TimelineEventTimestampView( + event = event, + onClick = onTimestampClicked, + onLongClick = ::onTimestampLongClick, + modifier = timestampModifier + .align(Alignment.End) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) + } } } - /** Used only for media items, with no reply to metadata. It displays the contents with no paddings. */ - @Composable - fun SimpleMediaItemLayout(modifier: Modifier = Modifier) { - ContentAndTimestampView(overlayTimestamp = true, modifier = modifier) - } - - /** Used for every other type of message, groups the different components in a Column with some space between them. */ + /** Groups the different components in a Column with some space between them. */ @Composable fun CommonLayout( inReplyToDetails: InReplyTo.Ready?, modifier: Modifier = Modifier ) { + var modifierWithPadding: Modifier = Modifier + var contentModifier: Modifier = Modifier EqualWidthColumn(modifier = modifier, spacing = 8.dp) { - if (inReplyToDetails != null) { - val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value - val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails) - val text = textForInReplyTo(inReplyToDetails) - ReplyToContent( - senderName = senderName, - text = text, - attachmentThumbnailInfo = attachmentThumbnailInfo, - modifier = Modifier - .padding(top = 8.dp, start = 8.dp, end = 8.dp) - .clip(RoundedCornerShape(6.dp)) - .clickable(enabled = true, onClick = inReplyToClick), - ) - } - val modifierWithPadding = if (isMediaItem) { - Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp) - } else { - Modifier - } - - val contentModifier = if (isMediaItem) { - Modifier.clip(RoundedCornerShape(12.dp)) - } else { - if (inReplyToDetails != null) { - Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp) - } else { - Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + when { + inReplyToDetails != null -> { + val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value + val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails) + val text = textForInReplyTo(inReplyToDetails) + ReplyToContent( + senderName = senderName, + text = text, + attachmentThumbnailInfo = attachmentThumbnailInfo, + modifier = Modifier + .padding(top = 8.dp, start = 8.dp, end = 8.dp) + .clip(RoundedCornerShape(6.dp)) + .clickable(enabled = true, onClick = inReplyToClick), + ) + if (timestampPosition == TimestampPosition.Overlay) { + modifierWithPadding = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp) + contentModifier = Modifier.clip(RoundedCornerShape(12.dp)) + } else { + contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp) + } + } + timestampPosition != TimestampPosition.Overlay -> { + contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) } } ContentAndTimestampView( - overlayTimestamp = isMediaItem, + timestampPosition = timestampPosition, contentModifier = contentModifier, modifier = modifierWithPadding, ) } } - if (isMediaItem && replyToDetails == null) { - SimpleMediaItemLayout() - } else { - CommonLayout(inReplyToDetails = replyToDetails, modifier = modifier) - } + CommonLayout(inReplyToDetails = replyToDetails, modifier = bubbleModifier) } @Composable @@ -810,3 +818,23 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight { onTimestampClicked = {}, ) } + +// Note: no need for light/dark variant for this preview, we only look at the timestamp position +@Preview +@Composable +internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight { + TimelineItemEventRow( + event = aTimelineItemEvent(content = aTimelineItemPollContent()), + isHighlighted = false, + canReply = true, + onClick = {}, + onLongClick = {}, + onUserDataClick = {}, + inReplyToClick = {}, + onReactionClick = { _, _ -> }, + onReactionLongClick = { _, _ -> }, + onMoreReactionsClick = {}, + onSwipeToReply = {}, + onTimestampClicked = {}, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimestampPosition.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimestampPosition.kt new file mode 100644 index 0000000000..7063e07635 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimestampPosition.kt @@ -0,0 +1,41 @@ +/* + * 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.impl.timeline.components + +enum class TimestampPosition { + /** + * Timestamp should overlay the timeline event content (eg. image). + */ + Overlay, + + /** + * Timestamp should be aligned with the timeline event content if this is possible (eg. text). + */ + Aligned, + + /** + * Timestamp should always be rendered below the timeline event content (eg. poll). + */ + Below; + + companion object { + /** + * Default timestamp position for timeline event contents. + */ + val Default: TimestampPosition = Aligned + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt index db3503be37..3608593cde 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentProvider -import io.element.android.features.poll.api.ActivePollContentView +import io.element.android.features.poll.api.PollContentView import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.matrix.api.poll.PollAnswer @@ -33,10 +33,11 @@ fun TimelineItemPollView( onAnswerSelected: (PollAnswer) -> Unit, modifier: Modifier = Modifier, ) { - ActivePollContentView( + PollContentView( question = content.question, answerItems = content.answerItems.toImmutableList(), pollKind = content.pollKind, + isPollEnded = content.isEnded, onAnswerSelected = onAnswerSelected, modifier = modifier, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt index 7b04d60ade..c6b0218bba 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt @@ -21,7 +21,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -35,6 +37,7 @@ 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.text.toAnnotatedString +import io.element.android.libraries.theme.ElementTheme @Composable fun TimelineItemTextView( @@ -45,31 +48,33 @@ fun TimelineItemTextView( onTextClicked: () -> Unit = {}, onTextLongClicked: () -> Unit = {}, ) { - val htmlDocument = content.htmlDocument - if (htmlDocument != null) { - // For now we ignore the extra padding for html content, so add some spacing - // below the content (as previous behavior) - Column(modifier = modifier) { - HtmlDocument( - document = htmlDocument, - modifier = Modifier, - onTextClicked = onTextClicked, - onTextLongClicked = onTextLongClicked, - interactionSource = interactionSource - ) - Spacer(Modifier.height(16.dp)) - } - } else { - Box(modifier) { - val textWithPadding = remember(content.body) { - content.body + extraPadding.getStr(16.sp).toAnnotatedString() + CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textPrimary) { + val htmlDocument = content.htmlDocument + if (htmlDocument != null) { + // For now we ignore the extra padding for html content, so add some spacing + // below the content (as previous behavior) + Column(modifier = modifier) { + HtmlDocument( + document = htmlDocument, + modifier = Modifier, + onTextClicked = onTextClicked, + onTextLongClicked = onTextLongClicked, + interactionSource = interactionSource + ) + Spacer(Modifier.height(16.dp)) + } + } else { + Box(modifier) { + val textWithPadding = remember(content.body) { + content.body + extraPadding.getStr(16.sp).toAnnotatedString() + } + ClickableLinkText( + text = textWithPadding, + onClick = onTextClicked, + onLongClick = onTextLongClicked, + interactionSource = interactionSource + ) } - ClickableLinkText( - text = textWithPadding, - onClick = onTextClicked, - onLongClick = onTextLongClicked, - interactionSource = interactionSource - ) } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryEvents.kt index fdf94f52ce..24583783b9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryEvents.kt @@ -20,6 +20,6 @@ import io.element.android.features.messages.impl.timeline.model.AggregatedReacti import io.element.android.libraries.matrix.api.core.EventId sealed interface ReactionSummaryEvents { - object Clear : ReactionSummaryEvents + data object Clear : ReactionSummaryEvents data class ShowReactionSummary(val eventId: EventId, val reactions: List, val selectedKey: String) : ReactionSummaryEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt index ab6e32f078..97ef92ceb8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt @@ -20,7 +20,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem sealed interface RetrySendMenuEvents { data class EventSelected(val event: TimelineItem.Event) : RetrySendMenuEvents - object RetrySend : RetrySendMenuEvents - object RemoveFailed : RetrySendMenuEvents - object Dismiss: RetrySendMenuEvents + data object RetrySend : RetrySendMenuEvents + data object RemoveFailed : RetrySendMenuEvents + data object Dismiss: RetrySendMenuEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt index 7c61466337..9c06b17056 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt @@ -23,7 +23,7 @@ import io.element.android.features.poll.api.PollAnswerItem import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.poll.isDisclosed import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import javax.inject.Inject @@ -36,27 +36,41 @@ class TimelineItemContentPollFactory @Inject constructor( if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent // Todo Move this computation to the matrix rust sdk - val showResults = content.kind == PollKind.Disclosed && matrixClient.sessionId in content.votes.flatMap { it.value } - val pollVotesCount = content.votes.flatMap { it.value }.size - val userVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys + val totalVoteCount = content.votes.flatMap { it.value }.size + val myVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys + val isEndedPoll = content.endTime != null + val winnerIds = if (!isEndedPoll) { + emptyList() + } else { + content.answers + .map { answer -> answer.id } + .groupBy { answerId -> content.votes[answerId]?.size ?: 0 } // Group by votes count + .maxByOrNull { (votes, _) -> votes } // Keep max voted answers + ?.takeIf { (votes, _) -> votes > 0 } // Ignore if no option has been voted + ?.value + .orEmpty() + } val answerItems = content.answers.map { answer -> - val votesCount = content.votes[answer.id]?.size ?: 0 - val progress = if (pollVotesCount > 0) votesCount.toFloat() / pollVotesCount.toFloat() else 0f + val answerVoteCount = content.votes[answer.id]?.size ?: 0 + val isSelected = answer.id in myVotes + val isWinner = answer.id in winnerIds + val percentage = if (totalVoteCount > 0) answerVoteCount.toFloat() / totalVoteCount.toFloat() else 0f PollAnswerItem( answer = answer, - isSelected = answer.id in userVotes, - isDisclosed = showResults, - votesCount = votesCount, - progress = progress, + isSelected = isSelected, + isEnabled = !isEndedPoll, + isWinner = isWinner, + isDisclosed = content.kind.isDisclosed || isEndedPoll, + votesCount = answerVoteCount, + percentage = percentage, ) } return TimelineItemPollContent( question = content.question, answerItems = answerItems, - votes = content.votes, pollKind = content.kind, - isDisclosed = showResults + isEnded = isEndedPoll, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemGroupPosition.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemGroupPosition.kt index 5a93e87e73..556493bd84 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemGroupPosition.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemGroupPosition.kt @@ -40,22 +40,22 @@ sealed interface TimelineItemGroupPosition { /** * The event is part of a group of events from the same sender and is the first sent Event. */ - object First : TimelineItemGroupPosition + data object First : TimelineItemGroupPosition /** * The event is part of a group of events from the same sender and is neither the first nor the last sent Event. */ - object Middle : TimelineItemGroupPosition + data object Middle : TimelineItemGroupPosition /** * The event is part of a group of events from the same sender and is the last sent Event. */ - object Last : TimelineItemGroupPosition + data object Last : TimelineItemGroupPosition /** * The event is not part of a group of events. Sender of previous event is different, and sender of next event is different. */ - object None : TimelineItemGroupPosition + data object None : TimelineItemGroupPosition /** * Return true if the previous sender of the event is a different sender. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt index b8a2fa8bca..0f94b97776 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt @@ -17,15 +17,13 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.features.poll.api.PollAnswerItem -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.poll.PollKind data class TimelineItemPollContent( val question: String, val answerItems: List, - val votes: Map>, val pollKind: PollKind, - val isDisclosed: Boolean, + val isEnded: Boolean, ) : TimelineItemEventContent { override val type: String = "TimelineItemPollContent" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt index 665d507ead..247d450ae7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt @@ -24,16 +24,15 @@ open class TimelineItemPollContentProvider : PreviewParameterProvider get() = sequenceOf( aTimelineItemPollContent(), - aTimelineItemPollContent().copy(isDisclosed = true), + aTimelineItemPollContent().copy(pollKind = PollKind.Undisclosed), ) } fun aTimelineItemPollContent(): TimelineItemPollContent { return TimelineItemPollContent( pollKind = PollKind.Disclosed, - isDisclosed = false, question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(), - votes = emptyMap(), + isEnded = false, ) } diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml index 8da8200bd5..7ca02ef21c 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -5,19 +5,43 @@ "%1$d změny místnosti" "%1$d změn místnosti" + + "%1$d další" + "%1$d další" + "%1$d dalších" + "Fotoaparát" "Vyfotit" "Natočit video" "Příloha" "Knihovna fotografií a videí" "Poloha" + "Hlasování" + "Historie zpráv je momentálně v této místnosti nedostupná" "Nepodařilo se načíst údaje o uživateli" "Chtěli byste je pozvat zpět?" "V tomto chatu jste sami" "Zpráva zkopírována" "Nemáte oprávnění zveřejňovat příspěvky v této místnosti" + "Povolit vlastní nastavení" + "Zapnutím této funkce přepíšete výchozí nastavení" + "Upozornit mě v tomto chatu na" + "Můžete změnit ve vašem %1$s." + "globální nastavení" + "Výchozí nastavení" + "Odebrat vlastní nastavení" + "Při načítání nastavení oznámení došlo k chybě." + "Obnovení výchozího režimu se nezdařilo, zkuste to prosím znovu." + "Nastavení režimu se nezdařilo, zkuste to prosím znovu." + "Všechny zprávy" + "Pouze zmínky a klíčová slova" + "V této místnosti mě upozornit na" + "Zobrazit méně" + "Zobrazit více" "Odeslat znovu" "Vaši zprávu se nepodařilo odeslat" + "Přidat emoji" + "Zobrazit méně" "Nahrání média se nezdařilo, zkuste to prosím znovu." "Odstranit" diff --git a/features/messages/impl/src/main/res/values-de/translations.xml b/features/messages/impl/src/main/res/values-de/translations.xml index 84c903844e..482065c887 100644 --- a/features/messages/impl/src/main/res/values-de/translations.xml +++ b/features/messages/impl/src/main/res/values-de/translations.xml @@ -10,16 +10,32 @@ "Anhang" "Foto- & Video-Bibliothek" "Standort" + "Umfrage" "Der Nachrichtenverlauf ist in diesem Raum derzeit nicht verfügbar" "Benutzerdetails konnten nicht abgerufen werden" "Möchtest du sie wieder einladen?" "Du bist allein in diesem Chat" "Nachricht kopiert" "Du bist keine Berechtigung, um in diesem Raum zu posten" + "Benutzerdefinierte Einstellung zulassen" + "Das Aktivieren dieser Option wird die Standardeinstellungen überschreiben." + "Benachrichtige mich in diesem Chat für" + "Du kannst es in deinem %1$s ändern." + "Globale Einstellungen" + "Standardeinstellung" + "Benutzerdefinierte Einstellung entfernen" + "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." + "Wiederherstellung des Standardmodus fehlgeschlagen. Bitte versuche es erneut." + "Fehler beim Einstellen des Modus. Bitte versuche es erneut." + "Alle Nachrichten" + "Nur Erwähnungen und Schlüsselwörter" + "In diesem Raum, benachrichtige mich für" "Weniger anzeigen" "Mehr anzeigen" "Erneut senden" "Ihre Nachricht konnte nicht gesendet werden" + "Emoji hinzufügen" + "Weniger anzeigen" "Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuche es erneut." "Entfernen" diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml index c3ed526564..33147f8ea2 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -5,6 +5,11 @@ "%1$d изменения в комнате" "%1$d изменений в комнате" + + "И ещё %1$d" + "И ещё %1$d" + "И ещё %1$d" + "Камера" "Сделать фото" "Записать видео" @@ -24,11 +29,13 @@ "Вы можете изменить его в своем %1$s." "Основные Настройки" "Настройка по умолчанию" + "Удалить пользовательскую настройку" "Произошла ошибка при загрузке настроек уведомлений." "Не удалось восстановить режим по умолчанию, попробуйте еще раз." "Не удалось настроить режим, попробуйте еще раз." "Все сообщения" "Только упоминания и ключевые слова" + "В этой комнате уведомить меня о" "Показать меньше" "Показать больше" "Отправить снова" diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml index 07e62e9a4d..03a32f847f 100644 --- a/features/messages/impl/src/main/res/values-sk/translations.xml +++ b/features/messages/impl/src/main/res/values-sk/translations.xml @@ -16,6 +16,7 @@ "Príloha" "Knižnica fotografií a videí" "Poloha" + "Anketa" "História správ v tejto miestnosti nie je momentálne k dispozícii" "Nepodarilo sa získať údaje o používateľovi" "Chceli by ste ich pozvať späť?" @@ -28,11 +29,13 @@ "Môžete to zmeniť vo svojich %1$s." "všeobecných nastaveniach" "Predvolené nastavenie" + "Odstrániť vlastné nastavenie" "Pri načítavaní nastavení oznámení došlo k chybe." "Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova." "Nepodarilo sa nastaviť režim, skúste to prosím znova." "Všetky správy" "Iba zmienky a kľúčové slová" + "V tejto miestnosti ma upozorniť na" "Zobraziť menej" "Zobraziť viac" "Odoslať znova" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 5fcd06a980..2c542e0054 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -21,7 +21,6 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.analytics.test.FakeAnalyticsService import io.element.android.features.messages.fixtures.aMessageEvent import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.InviteDialogAction @@ -69,6 +68,7 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilTimeout import io.element.android.tests.testutils.testCoroutineDispatchers diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt index afef9f6730..b3c805d32d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -26,10 +26,14 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent +import io.element.android.features.poll.api.PollAnswerItem +import io.element.android.libraries.matrix.api.poll.PollAnswer +import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.core.aBuildMeta import kotlinx.collections.immutable.persistentListOf @@ -369,6 +373,56 @@ class ActionListPresenterTest { assertThat(successState.displayEmojiReactions).isFalse() } } + + @Test + fun `present - compute for poll message`() = runTest { + val presenter = anActionListPresenter(isBuildDebuggable = false) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = true, + content = TimelineItemPollContent( + question = "Some question?", + answerItems = listOf( + PollAnswerItem( + answer = PollAnswer("id_1", "Answer1"), + isSelected = false, + isEnabled = false, + isWinner = false, + isDisclosed = false, + votesCount = 0, + percentage = 0.0f, + ), + PollAnswerItem( + answer = PollAnswer("id_2", "Answer2"), + isSelected = false, + isEnabled = false, + isWinner = false, + isDisclosed = false, + votesCount = 0, + percentage = 0.0f, + ), + ), + pollKind = PollKind.Disclosed, + isEnded = false, + ) + ) + + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + messageEvent, + persistentListOf( + TimelineItemAction.Redact, + ) + ) + ) + assertThat(successState.displayEmojiReactions).isTrue() + } + } } private fun anActionListPresenter(isBuildDebuggable: Boolean) = ActionListPresenter(buildMeta = aBuildMeta(isDebuggable = isBuildDebuggable)) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 25b506eb7f..799db7f274 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -24,7 +24,6 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.analytics.test.FakeAnalyticsService import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents @@ -55,6 +54,7 @@ import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.services.analytics.test.FakeAnalyticsService import io.mockk.mockk import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -551,5 +551,6 @@ fun anEditMode( message: String = A_MESSAGE, transactionId: TransactionId? = null, ) = MessageComposerMode.Edit(eventId, message, transactionId) + fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, AN_EVENT_ID, A_MESSAGE) fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt new file mode 100644 index 0000000000..a1411293e9 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt @@ -0,0 +1,279 @@ +/* + * 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.factories.event + +import com.google.common.truth.Truth +import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentPollFactory +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent +import io.element.android.features.poll.api.PollAnswerItem +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.poll.PollAnswer +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_10 +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID_3 +import io.element.android.libraries.matrix.test.A_USER_ID_4 +import io.element.android.libraries.matrix.test.A_USER_ID_5 +import io.element.android.libraries.matrix.test.A_USER_ID_6 +import io.element.android.libraries.matrix.test.A_USER_ID_7 +import io.element.android.libraries.matrix.test.A_USER_ID_8 +import io.element.android.libraries.matrix.test.A_USER_ID_9 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class TimelineItemContentPollFactoryTest { + + private val factory = TimelineItemContentPollFactory( + matrixClient = FakeMatrixClient(), + featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.Polls.key to true)), + ) + + @Test + fun `Disclosed poll - not ended, no votes`() = runTest { + Truth.assertThat(factory.create(aPollContent())).isEqualTo(aTimelineItemPollContent()) + } + + @Test + fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest { + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } + Truth.assertThat( + factory.create(aPollContent(votes = votes)) + ) + .isEqualTo( + aTimelineItemPollContent( + answerItems = listOf( + aPollAnswerItem(answer = A_POLL_ANSWER_1, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3), + aPollAnswerItem(answer = A_POLL_ANSWER_4, votesCount = 1, percentage = 0.1f), + ), + ) + ) + } + + @Test + fun `Disclosed poll - ended, no votes, no winner`() = runTest { + Truth.assertThat( + factory.create(aPollContent(endTime = 1UL)) + ).isEqualTo( + aTimelineItemPollContent().let { + it.copy( + answerItems = it.answerItems.map { answerItem -> answerItem.copy(isEnabled = false) }, + isEnded = true, + ) + } + ) + } + + @Test + fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest { + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } + Truth.assertThat( + factory.create(aPollContent(votes = votes, endTime = 1UL)) + ) + .isEqualTo( + aTimelineItemPollContent( + answerItems = listOf( + aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f), + ), + isEnded = true, + ) + ) + } + + @Test + fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { + val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id } + Truth.assertThat( + factory.create(aPollContent(votes = votes, endTime = 1UL)) + ) + .isEqualTo( + aTimelineItemPollContent( + answerItems = listOf( + aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + ), + isEnded = true, + ) + ) + } + + @Test + fun `Undisclosed poll - not ended, no votes`() = runTest { + Truth.assertThat( + factory.create(aPollContent(PollKind.Undisclosed).copy()) + ).isEqualTo( + aTimelineItemPollContent(PollKind.Undisclosed).let { + it.copy(answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) }) + } + ) + } + + @Test + fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest { + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } + Truth.assertThat( + factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes)) + ) + .isEqualTo( + aTimelineItemPollContent( + pollKind = PollKind.Undisclosed, + answerItems = listOf( + aPollAnswerItem(answer = A_POLL_ANSWER_1, isDisclosed = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isDisclosed = false, isSelected = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isDisclosed = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isDisclosed = false, votesCount = 1, percentage = 0.1f), + ), + ) + ) + } + + @Test + fun `Undisclosed poll - ended, no votes, no winner`() = runTest { + Truth.assertThat( + factory.create(aPollContent(pollKind = PollKind.Undisclosed, endTime = 1UL)) + ).isEqualTo( + aTimelineItemPollContent().let { + it.copy( + pollKind = PollKind.Undisclosed, + answerItems = it.answerItems.map { answerItem -> + answerItem.copy(isDisclosed = true, isEnabled = false, isWinner = false) + }, + isEnded = true, + ) + } + ) + } + + @Test + fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest { + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } + Truth.assertThat( + factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL)) + ) + .isEqualTo( + aTimelineItemPollContent( + pollKind = PollKind.Undisclosed, + answerItems = listOf( + aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f), + ), + isEnded = true, + ) + ) + } + + @Test + fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { + val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id } + Truth.assertThat( + factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL)) + ) + .isEqualTo( + aTimelineItemPollContent( + pollKind = PollKind.Undisclosed, + answerItems = listOf( + aPollAnswerItem(A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + aPollAnswerItem(A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f), + aPollAnswerItem(A_POLL_ANSWER_3, isEnabled = false), + aPollAnswerItem(A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + ), + isEnded = true, + ) + ) + } + + private fun aPollContent( + pollKind: PollKind = PollKind.Disclosed, + votes: Map> = emptyMap(), + endTime: ULong? = null, + ): PollContent = PollContent( + question = A_POLL_QUESTION, + kind = pollKind, + maxSelections = 1UL, + answers = listOf(A_POLL_ANSWER_1, A_POLL_ANSWER_2, A_POLL_ANSWER_3, A_POLL_ANSWER_4), + votes = votes, + endTime = endTime, + ) + + private fun aTimelineItemPollContent( + pollKind: PollKind = PollKind.Disclosed, + answerItems: List = listOf( + aPollAnswerItem(A_POLL_ANSWER_1), + aPollAnswerItem(A_POLL_ANSWER_2), + aPollAnswerItem(A_POLL_ANSWER_3), + aPollAnswerItem(A_POLL_ANSWER_4), + ), + isEnded: Boolean = false, + ) = TimelineItemPollContent( + question = A_POLL_QUESTION, + answerItems = answerItems, + pollKind = pollKind, + isEnded = isEnded, + ) + + private fun aPollAnswerItem( + answer: PollAnswer, + isSelected: Boolean = false, + isEnabled: Boolean = true, + isWinner: Boolean = false, + isDisclosed: Boolean = true, + votesCount: Int = 0, + percentage: Float = 0f, + ) = PollAnswerItem( + answer = answer, + isSelected = isSelected, + isEnabled = isEnabled, + isWinner = isWinner, + isDisclosed = isDisclosed, + votesCount = votesCount, + percentage = percentage, + ) + + private companion object TestData { + private const val A_POLL_QUESTION = "What is your favorite food?" + private val A_POLL_ANSWER_1 = PollAnswer("id_1", "Pizza") + private val A_POLL_ANSWER_2 = PollAnswer("id_2", "Pasta") + private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries") + private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger") + + private val MY_USER_WINNING_VOTES = mapOf( + A_POLL_ANSWER_1 to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), + A_POLL_ANSWER_2 to listOf(A_USER_ID /* my vote */, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), // winner + A_POLL_ANSWER_3 to emptyList(), + A_POLL_ANSWER_4 to listOf(A_USER_ID_10), + ) + private val OTHER_WINNING_VOTES = mapOf( + A_POLL_ANSWER_1 to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), // winner + A_POLL_ANSWER_2 to listOf(A_USER_ID /* my vote */, A_USER_ID_6), + A_POLL_ANSWER_3 to emptyList(), + A_POLL_ANSWER_4 to listOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), // winner + ) + } +} diff --git a/features/poll/api/build.gradle.kts b/features/poll/api/build.gradle.kts index be198ba740..6d94fa1b2f 100644 --- a/features/poll/api/build.gradle.kts +++ b/features/poll/api/build.gradle.kts @@ -27,8 +27,6 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) - implementation(libs.androidx.constraintlayout) - implementation(libs.androidx.constraintlayout.compose) implementation(projects.libraries.matrix.api) ksp(libs.showkase.processor) diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.kt deleted file mode 100644 index 587c3306b1..0000000000 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.poll.api - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.BarChart -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.preview.DayNightPreviews -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.matrix.api.poll.PollAnswer -import io.element.android.libraries.matrix.api.poll.PollKind -import io.element.android.libraries.theme.ElementTheme -import io.element.android.libraries.ui.strings.CommonStrings -import kotlinx.collections.immutable.ImmutableList - -@Composable -fun ActivePollContentView( - question: String, - answerItems: ImmutableList, - pollKind: PollKind, - onAnswerSelected: (PollAnswer) -> Unit, - modifier: Modifier = Modifier, -) { - val showResults = answerItems.any { it.isSelected } - Column( - modifier = modifier - .selectableGroup() - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Icon(imageVector = Icons.Default.BarChart, contentDescription = null) - Text( - text = question, - style = ElementTheme.typography.fontBodyLgMedium - ) - } - - answerItems.forEach { answerItem -> - PollAnswerView( - answerItem = answerItem, - onClick = { onAnswerSelected(answerItem.answer) } - ) - } - - val votesCount = answerItems.sumOf { it.votesCount } - when { - pollKind == PollKind.Undisclosed -> { - Text( - modifier = Modifier - .align(Alignment.Start) - .padding(start = 32.dp), - style = ElementTheme.typography.fontBodyXsRegular, - color = ElementTheme.colors.textSecondary, - text = stringResource(CommonStrings.common_poll_undisclosed_text), - ) - } - showResults -> { - Text( - modifier = Modifier.align(Alignment.End), - style = ElementTheme.typography.fontBodyXsRegular, - color = ElementTheme.colors.textSecondary, - text = stringResource(CommonStrings.common_poll_total_votes, votesCount), - ) - } - } - } -} - -@DayNightPreviews -@Composable -internal fun ActivePollContentNoResultsPreview() = ElementPreview { - ActivePollContentView( - question = "What type of food should we have at the party?", - answerItems = aPollAnswerItemList(isDisclosed = false), - pollKind = PollKind.Undisclosed, - onAnswerSelected = { }, - ) -} - -@DayNightPreviews -@Composable -internal fun ActivePollContentWithResultsPreview() = ElementPreview { - ActivePollContentView( - question = "What type of food should we have at the party?", - answerItems = aPollAnswerItemList(), - pollKind = PollKind.Disclosed, - onAnswerSelected = { }, - ) -} diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt index 24db33ad1f..1955701c5b 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt @@ -23,14 +23,18 @@ import io.element.android.libraries.matrix.api.poll.PollAnswer * * @property answer the poll answer. * @property isSelected whether the user has selected this answer. + * @property isEnabled whether the answer can be voted. + * @property isWinner whether this is the winner answer in the poll. * @property isDisclosed whether the votes for this answer should be disclosed. * @property votesCount the number of votes for this answer. - * @property progress the percentage of votes for this answer. + * @property percentage the percentage of votes for this answer. */ data class PollAnswerItem( val answer: PollAnswer, val isSelected: Boolean, + val isEnabled: Boolean, + val isWinner: Boolean, val isDisclosed: Boolean, val votesCount: Int, - val progress: Float, + val percentage: Float, ) diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt index 26fa6fbb71..3e52cc5fc7 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt @@ -16,110 +16,166 @@ package io.element.android.features.poll.api +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.selection.selectable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.RadioButtonUnchecked +import androidx.compose.material3.IconButtonDefaults import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import androidx.constraintlayout.compose.Visibility -import io.element.android.libraries.designsystem.preview.DayNightPreviews -import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.ElementThemedPreview +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconToggleButton import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator -import io.element.android.libraries.designsystem.theme.components.RadioButton import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.toEnabledColor import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonPlurals -@Suppress("DestructuringDeclarationWithTooManyEntries") // This is necessary to declare the constraints ids @Composable fun PollAnswerView( answerItem: PollAnswerItem, onClick: () -> Unit, modifier: Modifier = Modifier, ) { - ConstraintLayout( + Row( modifier - .wrapContentHeight() .fillMaxWidth() .selectable( selected = answerItem.isSelected, + enabled = answerItem.isEnabled, onClick = onClick, role = Role.RadioButton, ) ) { - val (radioButton, answerText, votesText, progressBar) = createRefs() - RadioButton( - modifier = Modifier.constrainAs(radioButton) { - top.linkTo(answerText.top) - bottom.linkTo(answerText.bottom) - start.linkTo(parent.start) - end.linkTo(answerText.start) - }, - selected = answerItem.isSelected, - onClick = null // null recommended for accessibility with screenreaders - ) - Text( - modifier = Modifier.constrainAs(answerText) { - width = Dimension.fillToConstraints - top.linkTo(parent.top) - start.linkTo(radioButton.end, margin = 8.dp) - end.linkTo(votesText.start) - bottom.linkTo(progressBar.top) - }, - text = answerItem.answer.text, - ) - Text( - modifier = Modifier.constrainAs(votesText) { - start.linkTo(answerText.end) - end.linkTo(parent.end) - bottom.linkTo(answerText.bottom) - visibility = if (answerItem.isDisclosed) Visibility.Visible else Visibility.Gone - }, - text = pluralStringResource( - id = CommonPlurals.common_poll_votes_count, - count = answerItem.votesCount, - answerItem.votesCount + IconToggleButton( + modifier = Modifier.size(22.dp), + checked = answerItem.isSelected, + enabled = answerItem.isEnabled, + colors = IconButtonDefaults.iconToggleButtonColors( + contentColor = ElementTheme.colors.iconSecondary, + checkedContentColor = ElementTheme.colors.iconPrimary, + disabledContentColor = ElementTheme.colors.iconDisabled, ), - style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.colors.textSecondary, - ) - LinearProgressIndicator( - progress = answerItem.progress, - modifier = Modifier - .constrainAs(progressBar) { - start.linkTo(answerText.start) - end.linkTo(votesText.end) - top.linkTo(answerText.bottom, margin = 10.dp) - bottom.linkTo(parent.bottom) - width = Dimension.fillToConstraints - visibility = if (answerItem.isDisclosed) Visibility.Visible else Visibility.Gone - + onCheckedChange = { onClick() }, + ) { + Icon( + imageVector = if (answerItem.isSelected) { + Icons.Default.CheckCircle + } else { + Icons.Default.RadioButtonUnchecked }, - strokeCap = StrokeCap.Round, - ) + contentDescription = null, + ) + } + Spacer(modifier = Modifier.width(12.dp)) + Column { + Row { + Text( + modifier = Modifier.weight(1f), + text = answerItem.answer.text, + style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular, + ) + if (answerItem.isDisclosed) { + Text( + modifier = Modifier.align(Alignment.Bottom), + text = pluralStringResource( + id = CommonPlurals.common_poll_votes_count, + count = answerItem.votesCount, + answerItem.votesCount + ), + style = if (answerItem.isWinner) ElementTheme.typography.fontBodySmMedium else ElementTheme.typography.fontBodySmRegular, + color = if (answerItem.isWinner) ElementTheme.colors.textPrimary else ElementTheme.colors.textSecondary, + ) + } + } + Spacer(modifier = Modifier.height(10.dp)) + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + color = if (answerItem.isWinner) ElementTheme.colors.textSuccessPrimary else answerItem.isEnabled.toEnabledColor(), + progress = when { + answerItem.isDisclosed -> answerItem.percentage + answerItem.isSelected -> 1f + else -> 0f + }, + strokeCap = StrokeCap.Round, + ) + } } } -@DayNightPreviews +@Preview @Composable -internal fun PollAnswerViewNoResultsPreview() = ElementPreview { +internal fun PollAnswerDisclosedNotSelectedPreview() = ElementThemedPreview { PollAnswerView( - answerItem = aPollAnswerItem(), + answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false), onClick = { }, ) } -@DayNightPreviews +@Preview @Composable -internal fun PollAnswerViewWithResultPreview() = ElementPreview { +internal fun PollAnswerDisclosedSelectedPreview() = ElementThemedPreview { PollAnswerView( - answerItem = aPollAnswerItem(isDisclosed = true), + answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true), + onClick = { } + ) +} + +@Preview +@Composable +internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementThemedPreview { + PollAnswerView( + answerItem = aPollAnswerItem(isDisclosed = false, isSelected = false), + onClick = { }, + ) +} + +@Preview +@Composable +internal fun PollAnswerUndisclosedSelectedPreview() = ElementThemedPreview { + PollAnswerView( + answerItem = aPollAnswerItem(isDisclosed = false, isSelected = true), + onClick = { } + ) +} + +@Preview +@Composable +internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementThemedPreview { + PollAnswerView( + answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false, isEnabled = false, isWinner = true), + onClick = { } + ) +} + +@Preview +@Composable +internal fun PollAnswerEndedWinnerSelectedPreview() = ElementThemedPreview { + PollAnswerView( + answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = true), + onClick = { } + ) +} + +@Preview +@Composable +internal fun PollAnswerEndedSelectedPreview() = ElementThemedPreview { + PollAnswerView( + answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = false), onClick = { } ) } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt index 062d09fd88..e94b5adeeb 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt @@ -19,27 +19,33 @@ package io.element.android.features.poll.api import io.element.android.libraries.matrix.api.poll.PollAnswer import kotlinx.collections.immutable.persistentListOf -fun aPollAnswerItemList(isDisclosed: Boolean = true) = persistentListOf( +fun aPollAnswerItemList(isEnded: Boolean = false, isDisclosed: Boolean = true) = persistentListOf( aPollAnswerItem( answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"), isDisclosed = isDisclosed, + isEnabled = !isEnded, + isWinner = isEnded, votesCount = 5, - progress = 0.5f + percentage = 0.5f ), aPollAnswerItem( answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"), isDisclosed = isDisclosed, + isEnabled = !isEnded, + isWinner = false, votesCount = 0, - progress = 0f + percentage = 0f ), aPollAnswerItem( answer = PollAnswer("option_3", "Brazilian \uD83C\uDDE7\uD83C\uDDF7"), isDisclosed = isDisclosed, + isEnabled = !isEnded, + isWinner = false, isSelected = true, votesCount = 1, - progress = 0.1f + percentage = 0.1f ), - aPollAnswerItem(isDisclosed = isDisclosed), + aPollAnswerItem(isDisclosed = isDisclosed, isEnabled = !isEnded), ) fun aPollAnswerItem( @@ -48,13 +54,17 @@ fun aPollAnswerItem( "French \uD83C\uDDEB\uD83C\uDDF7 But make it a very very very long option then this should just keep expanding" ), isSelected: Boolean = false, + isEnabled: Boolean = true, + isWinner: Boolean = false, isDisclosed: Boolean = true, votesCount: Int = 4, - progress: Float = 0.4f, + percentage: Float = 0.4f, ) = PollAnswerItem( answer = answer, isSelected = isSelected, + isEnabled = isEnabled, + isWinner = isWinner, isDisclosed = isDisclosed, votesCount = votesCount, - progress = progress + percentage = percentage ) diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt new file mode 100644 index 0000000000..419aa21204 --- /dev/null +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt @@ -0,0 +1,167 @@ +/* + * 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.poll.api + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Poll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.poll.PollAnswer +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun PollContentView( + question: String, + answerItems: ImmutableList, + pollKind: PollKind, + isPollEnded: Boolean, + onAnswerSelected: (PollAnswer) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .selectableGroup() + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + PollTitle(title = question) + + PollAnswers(answerItems = answerItems, onAnswerSelected = onAnswerSelected) + + when { + isPollEnded || pollKind == PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems) + pollKind == PollKind.Undisclosed -> UndisclosedPollBottomNotice() + } + } +} + +@Composable +internal fun PollTitle( + title: String, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Icon( + modifier = Modifier.size(22.dp), + imageVector = Icons.Outlined.Poll, + contentDescription = null + ) + Text( + text = title, + style = ElementTheme.typography.fontBodyLgMedium + ) + } +} + +@Composable +internal fun PollAnswers( + answerItems: ImmutableList, + onAnswerSelected: (PollAnswer) -> Unit, + modifier: Modifier = Modifier, +) { + + answerItems.forEach { answerItem -> + PollAnswerView( + modifier = modifier, + answerItem = answerItem, + onClick = { onAnswerSelected(answerItem.answer) } + ) + } +} + +@Composable +internal fun ColumnScope.DisclosedPollBottomNotice( + answerItems: ImmutableList, + modifier: Modifier = Modifier +) { + val votesCount = answerItems.sumOf { it.votesCount } + Text( + modifier = modifier.align(Alignment.End), + style = ElementTheme.typography.fontBodyXsRegular, + color = ElementTheme.colors.textSecondary, + text = stringResource(CommonStrings.common_poll_total_votes, votesCount), + ) +} + +@Composable +fun ColumnScope.UndisclosedPollBottomNotice(modifier: Modifier = Modifier) { + Text( + modifier = modifier + .align(Alignment.Start) + .padding(start = 34.dp), + style = ElementTheme.typography.fontBodyXsRegular, + color = ElementTheme.colors.textSecondary, + text = stringResource(CommonStrings.common_poll_undisclosed_text), + ) +} + +@DayNightPreviews +@Composable +internal fun PollContentUndisclosedPreview() = ElementPreview { + PollContentView( + question = "What type of food should we have at the party?", + answerItems = aPollAnswerItemList(isDisclosed = false), + pollKind = PollKind.Undisclosed, + isPollEnded = false, + onAnswerSelected = { }, + ) +} + +@DayNightPreviews +@Composable +internal fun PollContentDisclosedPreview() = ElementPreview { + PollContentView( + question = "What type of food should we have at the party?", + answerItems = aPollAnswerItemList(), + pollKind = PollKind.Disclosed, + isPollEnded = false, + onAnswerSelected = { }, + ) +} + +@DayNightPreviews +@Composable +internal fun PollContentEndedPreview() = ElementPreview { + PollContentView( + question = "What type of food should we have at the party?", + answerItems = aPollAnswerItemList(isEnded = true), + pollKind = PollKind.Disclosed, + isPollEnded = false, + onAnswerSelected = { }, + ) +} diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollEntryPoint.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt similarity index 65% rename from features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollEntryPoint.kt rename to features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt index d8f2aed846..abbb041374 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollEntryPoint.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt @@ -14,24 +14,12 @@ * limitations under the License. */ -package io.element.android.features.poll.api +package io.element.android.features.poll.api.create import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint -interface PollEntryPoint : FeatureEntryPoint { - - fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder - - interface NodeBuilder { - fun callback(callback: Callback): NodeBuilder - fun build(): Node - } - - interface Callback : Plugin { - // Add your callbacks - } +interface CreatePollEntryPoint : FeatureEntryPoint { + fun createNode(parentNode: Node, buildContext: BuildContext): Node } - diff --git a/features/poll/impl/build.gradle.kts b/features/poll/impl/build.gradle.kts index 626a7d0f2c..4e8d36966a 100644 --- a/features/poll/impl/build.gradle.kts +++ b/features/poll/impl/build.gradle.kts @@ -14,8 +14,6 @@ * limitations under the License. */ -// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed -@Suppress("DSL_SCOPE_VIOLATION") plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) @@ -40,6 +38,8 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) + implementation(projects.services.analytics.api) + implementation(projects.libraries.uiStrings) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -47,6 +47,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.services.analytics.test) ksp(libs.showkase.processor) } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt new file mode 100644 index 0000000000..1251e07696 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt @@ -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.poll.impl.create + +import io.element.android.libraries.matrix.api.poll.PollKind + +sealed interface CreatePollEvents { + data object Create : CreatePollEvents + data class SetQuestion(val question: String) : CreatePollEvents + data class SetAnswer(val index: Int, val text: String) : CreatePollEvents + data object AddAnswer : CreatePollEvents + data class RemoveAnswer(val index: Int) : CreatePollEvents + data class SetPollKind(val pollKind: PollKind) : CreatePollEvents + data object NavBack : CreatePollEvents + data object ConfirmNavBack : CreatePollEvents + data object HideConfirmation : CreatePollEvents +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt new file mode 100644 index 0000000000..387f57a597 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt @@ -0,0 +1,56 @@ +/* + * 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.poll.impl.create + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.lifecycle.subscribe +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.RoomScope + +@ContributesNode(RoomScope::class) +class CreatePollNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: CreatePollPresenter.Factory, + // analyticsService: AnalyticsService, // TODO Polls: add analytics +) : Node(buildContext, plugins = plugins) { + + private val presenter = presenterFactory.create(backNavigator = ::navigateUp) + + init { + lifecycle.subscribe( + onResume = { + // TODO Polls: add analytics + // analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.PollView)) + } + ) + } + + @Composable + override fun View(modifier: Modifier) { + CreatePollView( + state = presenter.present(), + modifier = modifier, + ) + } +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt new file mode 100644 index 0000000000..b44afae9d1 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt @@ -0,0 +1,162 @@ +/* + * 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.poll.impl.create + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.room.MatrixRoom +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.launch +import timber.log.Timber + +private const val MIN_ANSWERS = 2 +private const val MAX_ANSWERS = 20 +private const val MAX_ANSWER_LENGTH = 240 +private const val MAX_SELECTIONS = 1 + +class CreatePollPresenter @AssistedInject constructor( + private val room: MatrixRoom, + // private val analyticsService: AnalyticsService, // TODO Polls: add analytics + @Assisted private val navigateUp: () -> Unit, + // private val messageComposerContext: MessageComposerContext, // TODO Polls: add analytics +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(backNavigator: () -> Unit): CreatePollPresenter + } + + @Composable + override fun present(): CreatePollState { + + var question: String by rememberSaveable { mutableStateOf("") } + var answers: List by rememberSaveable() { mutableStateOf(listOf("", "")) } + var pollKind: PollKind by rememberSaveable(saver = pollKindSaver) { mutableStateOf(PollKind.Disclosed) } + var showConfirmation: Boolean by rememberSaveable { mutableStateOf(false) } + + val canCreate: Boolean by remember { derivedStateOf { canCreate(question, answers) } } + val canAddAnswer: Boolean by remember { derivedStateOf { canAddAnswer(answers) } } + val immutableAnswers: ImmutableList by remember { derivedStateOf { answers.toAnswers() } } + + val scope = rememberCoroutineScope() + + fun handleEvents(event: CreatePollEvents) { + when (event) { + is CreatePollEvents.Create -> scope.launch { + if (canCreate) { + room.createPoll( + question = question, + answers = answers, + maxSelections = MAX_SELECTIONS, + pollKind = pollKind, + ) + // analyticsService.capture(PollCreate()) // TODO Polls: add analytics + navigateUp() + } else { + Timber.d("Cannot create poll") + } + } + is CreatePollEvents.AddAnswer -> { + answers = answers + "" + } + is CreatePollEvents.RemoveAnswer -> { + answers = answers.filterIndexed { index, _ -> index != event.index } + } + is CreatePollEvents.SetAnswer -> { + answers = answers.toMutableList().apply { + this[event.index] = event.text.take(MAX_ANSWER_LENGTH) + } + } + is CreatePollEvents.SetPollKind -> { + pollKind = event.pollKind + } + is CreatePollEvents.SetQuestion -> { + question = event.question + } + is CreatePollEvents.NavBack -> { + navigateUp() + } + CreatePollEvents.ConfirmNavBack -> { + val shouldConfirm = question.isNotBlank() || answers.any { it.isNotBlank() } + if (shouldConfirm) { + showConfirmation = true + } else { + navigateUp() + } + } + is CreatePollEvents.HideConfirmation -> showConfirmation = false + } + } + + return CreatePollState( + canCreate = canCreate, + canAddAnswer = canAddAnswer, + question = question, + answers = immutableAnswers, + pollKind = pollKind, + showConfirmation = showConfirmation, + eventSink = ::handleEvents, + ) + } +} + +private fun canCreate( + question: String, + answers: List +) = question.isNotBlank() && answers.size >= MIN_ANSWERS && answers.all { it.isNotBlank() } + +private fun canAddAnswer(answers: List) = answers.size < MAX_ANSWERS + +private fun List.toAnswers(): ImmutableList { + return map { answer -> + Answer( + text = answer, + canDelete = this.size > MIN_ANSWERS, + ) + }.toImmutableList() +} + +private val pollKindSaver: Saver, Boolean> = Saver( + save = { + when (it.value) { + PollKind.Disclosed -> false + PollKind.Undisclosed -> true + } + }, + restore = { + mutableStateOf( + when(it) { + true -> PollKind.Undisclosed + else -> PollKind.Disclosed + } + ) + } +) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt new file mode 100644 index 0000000000..eccaea45fc --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt @@ -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.poll.impl.create + +import io.element.android.libraries.matrix.api.poll.PollKind +import kotlinx.collections.immutable.ImmutableList + +data class CreatePollState( + val canCreate: Boolean, + val canAddAnswer: Boolean, + val question: String, + val answers: ImmutableList, + val pollKind: PollKind, + val showConfirmation: Boolean, + val eventSink: (CreatePollEvents) -> Unit = {}, +) + +data class Answer( + val text: String, + val canDelete: Boolean, +) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt new file mode 100644 index 0000000000..29aa1288ad --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt @@ -0,0 +1,124 @@ +/* + * 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.poll.impl.create + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.poll.PollKind +import kotlinx.collections.immutable.persistentListOf + +class CreatePollStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + CreatePollState( + canCreate = false, + canAddAnswer = true, + question = "", + answers = persistentListOf( + Answer("", false), + Answer("", false) + ), + pollKind = PollKind.Disclosed, + showConfirmation = false, + ), + CreatePollState( + canCreate = true, + canAddAnswer = true, + question = "What type of food should we have?", + answers = persistentListOf( + Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", false), + Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", false), + ), + showConfirmation = false, + pollKind = PollKind.Undisclosed, + ), + CreatePollState( + canCreate = true, + canAddAnswer = true, + question = "What type of food should we have?", + answers = persistentListOf( + Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", false), + Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", false), + ), + showConfirmation = true, + pollKind = PollKind.Undisclosed, + ), + CreatePollState( + canCreate = true, + canAddAnswer = true, + question = "What type of food should we have?", + answers = persistentListOf( + Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", true), + Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", true), + Answer("Brazilian \uD83C\uDDE7\uD83C\uDDF7", true), + Answer("French \uD83C\uDDEB\uD83C\uDDF7", true), + ), + showConfirmation = false, + pollKind = PollKind.Undisclosed, + ), + CreatePollState( + canCreate = true, + canAddAnswer = false, + question = "Should there be more than 20 answers?", + answers = persistentListOf( + Answer("1", true), + Answer("2", true), + Answer("3", true), + Answer("4", true), + Answer("5", true), + Answer("6", true), + Answer("7", true), + Answer("8", true), + Answer("9", true), + Answer("10", true), + Answer("11", true), + Answer("12", true), + Answer("13", true), + Answer("14", true), + Answer("15", true), + Answer("16", true), + Answer("17", true), + Answer("18", true), + Answer("19", true), + Answer("20", true), + ), + showConfirmation = false, + pollKind = PollKind.Undisclosed, + ), + CreatePollState( + canCreate = true, + canAddAnswer = true, + question = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + + " Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor" + + " in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt" + + " in culpa qui officia deserunt mollit anim id est laborum.", + answers = persistentListOf( + Answer( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + + " Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis a.", + false + ), + Answer( + "Laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore" + + " eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mol.", + false + ), + ), + showConfirmation = false, + pollKind = PollKind.Undisclosed, + ) + ) +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt new file mode 100644 index 0000000000..3e3ec4eb16 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt @@ -0,0 +1,187 @@ +/* + * 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.poll.impl.create + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.poll.impl.R +import io.element.android.libraries.designsystem.VectorIcons +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListItemStyle +import io.element.android.libraries.designsystem.theme.components.OutlinedTextField +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreatePollView( + state: CreatePollState, + modifier: Modifier = Modifier, +) { + val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) } + BackHandler(onBack = navBack) + if (state.showConfirmation) ConfirmationDialog( + content = stringResource(id = R.string.screen_create_poll_confirmation), + onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) }, + onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } + ) + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(id = R.string.screen_create_poll_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { + BackButton(onClick = navBack) + }, + actions = { + TextButton( + text = stringResource(id = CommonStrings.action_create), + onClick = { state.eventSink(CreatePollEvents.Create) }, + enabled = state.canCreate, + ) + } + ) + }, + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .consumeWindowInsets(paddingValues) + .imePadding() + .fillMaxSize(), + ) { + item { + Text( + text = stringResource(id = R.string.screen_create_poll_question_desc), + modifier = Modifier.padding(start = 32.dp), + style = ElementTheme.typography.fontBodyMdRegular, + ) + } + item { + ListItem( + headlineContent = { + OutlinedTextField( + value = state.question, + onValueChange = { + state.eventSink(CreatePollEvents.SetQuestion(it)) + }, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text(text = stringResource(id = R.string.screen_create_poll_question_hint)) + }, + ) + } + ) + } + itemsIndexed(state.answers) { index, answer -> + ListItem( + headlineContent = { + OutlinedTextField( + value = answer.text, + onValueChange = { + state.eventSink(CreatePollEvents.SetAnswer(index, it)) + }, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text(text = stringResource(id = R.string.screen_create_poll_answer_hint, index + 1)) + }, + ) + }, + trailingContent = ListItemContent.Custom { + Icon( + resourceId = VectorIcons.Delete, + contentDescription = null, + modifier = Modifier.clickable(answer.canDelete) { + state.eventSink(CreatePollEvents.RemoveAnswer(index)) + }, + ) + }, + style = if (answer.canDelete) ListItemStyle.Destructive else ListItemStyle.Default, + ) + } + if (state.canAddAnswer) { + item { + ListItem( + headlineContent = { Text(text = stringResource(id = R.string.screen_create_poll_add_option_btn)) }, + leadingContent = ListItemContent.Icon( + iconSource = IconSource.Vector(Icons.Default.Add), + ), + style = ListItemStyle.Primary, + onClick = { state.eventSink(CreatePollEvents.AddAnswer) }, + ) + } + } + item { + HorizontalDivider() + } + item { + ListItem( + headlineContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_headline)) }, + supportingContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_desc)) }, + trailingContent = ListItemContent.Switch( + checked = state.pollKind == PollKind.Undisclosed, + onChange = { state.eventSink(CreatePollEvents.SetPollKind(if (it) PollKind.Undisclosed else PollKind.Disclosed)) }, + ), + ) + } + } + } +} + +@DayNightPreviews +@Composable +internal fun CreatePollViewPreview( + @PreviewParameter(CreatePollStateProvider::class) state: CreatePollState +) = ElementPreview { + CreatePollView( + state = state, + ) +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/DefaultPollEntryPoint.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt similarity index 55% rename from features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/DefaultPollEntryPoint.kt rename to features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt index 052c1bcd5f..1ce64deb88 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/DefaultPollEntryPoint.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt @@ -14,33 +14,19 @@ * limitations under the License. */ -package io.element.android.features.poll.impl +package io.element.android.features.poll.impl.create import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.poll.api.PollEntryPoint +import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultPollEntryPoint @Inject constructor() : PollEntryPoint { - - override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): PollEntryPoint.NodeBuilder { - val plugins = ArrayList() - - return object : PollEntryPoint.NodeBuilder { - - override fun callback(callback: PollEntryPoint.Callback): PollEntryPoint.NodeBuilder { - plugins += callback - return this - } - - override fun build(): Node { - return parentNode.createNode(buildContext, plugins) - } - } +class DefaultCreatePollEntryPoint @Inject constructor() : CreatePollEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext): Node { + return parentNode.createNode(buildContext) } } diff --git a/features/poll/impl/src/main/res/values/localazy.xml b/features/poll/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..846b247108 --- /dev/null +++ b/features/poll/impl/src/main/res/values/localazy.xml @@ -0,0 +1,11 @@ + + + "Add option" + "Show results only after poll ends" + "Anonymous Poll" + "Option %1$d" + "Are you sure you would like to go back?" + "Question or topic" + "What is the poll about?" + "Create Poll" + diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt new file mode 100644 index 0000000000..9dacc6062d --- /dev/null +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt @@ -0,0 +1,239 @@ +/* + * 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.poll.impl.create + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.test.room.CreatePollInvocation +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CreatePollPresenterTest { + + private var navUpInvocationsCount = 0 + private val fakeMatrixRoom = FakeMatrixRoom() + // private val fakeAnalyticsService = FakeAnalyticsService() // TODO Polls: add analytics + + private val presenter = CreatePollPresenter( + room = fakeMatrixRoom, + // analyticsService = fakeAnalyticsService, // TODO Polls: add analytics + navigateUp = { navUpInvocationsCount++ }, + ) + + @Test + fun `default state has proper default values`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().let { + Truth.assertThat(it.canCreate).isEqualTo(false) + Truth.assertThat(it.canAddAnswer).isEqualTo(true) + Truth.assertThat(it.question).isEqualTo("") + Truth.assertThat(it.answers).isEqualTo(listOf(Answer("", false), Answer("", false))) + Truth.assertThat(it.pollKind).isEqualTo(PollKind.Disclosed) + Truth.assertThat(it.showConfirmation).isEqualTo(false) + } + } + } + + @Test + fun `non blank question and 2 answers are required to create a poll`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + Truth.assertThat(initial.canCreate).isEqualTo(false) + + initial.eventSink(CreatePollEvents.SetQuestion("A question?")) + val questionSet = awaitItem() + Truth.assertThat(questionSet.canCreate).isEqualTo(false) + + questionSet.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1")) + val answer1Set = awaitItem() + Truth.assertThat(answer1Set.canCreate).isEqualTo(false) + + answer1Set.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2")) + val answer2Set = awaitItem() + Truth.assertThat(answer2Set.canCreate).isEqualTo(true) + } + } + + @Test + fun `create polls sends a poll start event`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + initial.eventSink(CreatePollEvents.SetQuestion("A question?")) + initial.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1")) + initial.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2")) + skipItems(3) + initial.eventSink(CreatePollEvents.Create) + delay(1) // Wait for the coroutine to finish + Truth.assertThat(fakeMatrixRoom.createPollInvocations.size).isEqualTo(1) + Truth.assertThat(fakeMatrixRoom.createPollInvocations.last()).isEqualTo( + CreatePollInvocation( + question = "A question?", + answers = listOf("Answer 1", "Answer 2"), + maxSelections = 1, + pollKind = PollKind.Disclosed + ) + ) + } + } + + @Test + fun `add answer button adds an empty answer and removing it removes it`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + Truth.assertThat(initial.answers.size).isEqualTo(2) + + initial.eventSink(CreatePollEvents.AddAnswer) + val answerAdded = awaitItem() + Truth.assertThat(answerAdded.answers.size).isEqualTo(3) + Truth.assertThat(answerAdded.answers[2].text).isEqualTo("") + + initial.eventSink(CreatePollEvents.RemoveAnswer(2)) + val answerRemoved = awaitItem() + Truth.assertThat(answerRemoved.answers.size).isEqualTo(2) + } + } + + @Test + fun `set question sets the question`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + initial.eventSink(CreatePollEvents.SetQuestion("A question?")) + val questionSet = awaitItem() + Truth.assertThat(questionSet.question).isEqualTo("A question?") + } + } + + @Test + fun `set poll answer sets the given poll answer`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + initial.eventSink(CreatePollEvents.SetAnswer(0, "This is answer 1")) + val answerSet = awaitItem() + Truth.assertThat(answerSet.answers.first().text).isEqualTo("This is answer 1") + } + } + + @Test + fun `set poll kind sets the poll kind`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + initial.eventSink(CreatePollEvents.SetPollKind(PollKind.Undisclosed)) + val kindSet = awaitItem() + Truth.assertThat(kindSet.pollKind).isEqualTo(PollKind.Undisclosed) + } + } + + @Test + fun `can add options when between 2 and 20 and then no more`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + Truth.assertThat(initial.canAddAnswer).isEqualTo(true) + repeat(17) { + initial.eventSink(CreatePollEvents.AddAnswer) + Truth.assertThat(awaitItem().canAddAnswer).isEqualTo(true) + } + initial.eventSink(CreatePollEvents.AddAnswer) + Truth.assertThat(awaitItem().canAddAnswer).isEqualTo(false) + } + } + + @Test + fun `can delete option if there are more than 2`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + Truth.assertThat(initial.answers.all { it.canDelete }).isEqualTo(false) + initial.eventSink(CreatePollEvents.AddAnswer) + Truth.assertThat(awaitItem().answers.all { it.canDelete }).isEqualTo(true) + } + } + + @Test + fun `option with more than 240 char is truncated`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + initial.eventSink(CreatePollEvents.SetAnswer(0, "A".repeat(241))) + Truth.assertThat(awaitItem().answers.first().text.length).isEqualTo(240) + } + } + + @Test + fun `navBack event calls navBack lambda`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) + initial.eventSink(CreatePollEvents.NavBack) + Truth.assertThat(navUpInvocationsCount).isEqualTo(1) + } + } + + @Test + fun `confirm nav back with blank fields calls nav back lambda`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) + Truth.assertThat(initial.showConfirmation).isEqualTo(false) + initial.eventSink(CreatePollEvents.ConfirmNavBack) + Truth.assertThat(navUpInvocationsCount).isEqualTo(1) + } + } + + @Test + fun `confirm nav back with non blank fields shows confirmation dialog and sending hide hids it`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + initial.eventSink(CreatePollEvents.SetQuestion("Non blank")) + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) + Truth.assertThat(awaitItem().showConfirmation).isEqualTo(false) + initial.eventSink(CreatePollEvents.ConfirmNavBack) + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) + Truth.assertThat(awaitItem().showConfirmation).isEqualTo(true) + initial.eventSink(CreatePollEvents.HideConfirmation) + Truth.assertThat(awaitItem().showConfirmation).isEqualTo(false) + } + } +} diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 4ffca9d239..2773379ccc 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -66,7 +66,7 @@ dependencies { testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.impl) testImplementation(projects.features.logout.impl) - testImplementation(projects.features.analytics.test) + testImplementation(projects.services.analytics.test) testImplementation(projects.features.analytics.impl) testImplementation(projects.tests.testutils) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index 00684dde13..51e25fbebf 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -56,16 +56,17 @@ class PreferencesFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - object Root : NavTarget + data object Root : NavTarget @Parcelize - object DeveloperSettings : NavTarget + data object DeveloperSettings : NavTarget @Parcelize - object AnalyticsSettings : NavTarget + data object AnalyticsSettings : NavTarget @Parcelize - object About : NavTarget + + data object About : NavTarget @Parcelize object NotificationSettings : NavTarget diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/ElementLegal.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/ElementLegal.kt index e09e0df8f8..e54b7b9674 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/ElementLegal.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/ElementLegal.kt @@ -27,9 +27,9 @@ sealed class ElementLegal( @StringRes val titleRes: Int, val url: String, ) { - object Copyright : ElementLegal(CommonStrings.common_copyright, COPYRIGHT_URL) - object AcceptableUsePolicy : ElementLegal(CommonStrings.common_acceptable_use_policy, USE_POLICY_URL) - object PrivacyPolicy : ElementLegal(CommonStrings.common_privacy_policy, PRIVACY_URL) + data object Copyright : ElementLegal(CommonStrings.common_copyright, COPYRIGHT_URL) + data object AcceptableUsePolicy : ElementLegal(CommonStrings.common_acceptable_use_policy, USE_POLICY_URL) + data object PrivacyPolicy : ElementLegal(CommonStrings.common_privacy_policy, PRIVACY_URL) } fun getAllLegals(): List { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt index bb3879b129..ce67916178 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt @@ -20,5 +20,5 @@ import io.element.android.libraries.featureflag.ui.model.FeatureUiModel sealed interface DeveloperSettingsEvents { data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents - object ClearCache: DeveloperSettingsEvents + data object ClearCache: DeveloperSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index d4783cde1f..c601041a7d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -58,7 +58,7 @@ fun PreferencesRootView( onOpenRageShake: () -> Unit, onOpenAbout: () -> Unit, onOpenDeveloperSettings: () -> Unit, - onSuccessLogout: (String?) -> Unit, + onSuccessLogout: (logoutUrlResult: String?) -> Unit, onOpenNotificationSettings: () -> Unit, modifier: Modifier = Modifier, ) { diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsAnalyticsSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenterTest.kt similarity index 93% rename from features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsAnalyticsSettingsPresenterTest.kt rename to features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenterTest.kt index 29cc25e5be..2a7fdba258 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsAnalyticsSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsPresenterTest.kt @@ -21,12 +21,12 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.analytics.impl.preferences.DefaultAnalyticsPreferencesPresenter -import io.element.android.features.analytics.test.FakeAnalyticsService import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.services.analytics.test.FakeAnalyticsService import kotlinx.coroutines.test.runTest import org.junit.Test -class AnalyticsAnalyticsSettingsPresenterTest { +class AnalyticsSettingsPresenterTest { @Test fun `present - initial state`() = runTest { val analyticsPresenter = DefaultAnalyticsPreferencesPresenter(FakeAnalyticsService(), aBuildMeta()) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 5d508912a8..9ad5db3d1b 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -20,7 +20,6 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.analytics.test.FakeAnalyticsService import io.element.android.features.logout.impl.DefaultLogoutPreferencePresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.meta.BuildType @@ -30,6 +29,7 @@ import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.services.analytics.test.FakeAnalyticsService import kotlinx.coroutines.test.runTest import org.junit.Test diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDetectionEvents.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDetectionEvents.kt index 055a8339f6..8320d801b0 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDetectionEvents.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDetectionEvents.kt @@ -17,6 +17,6 @@ package io.element.android.features.rageshake.api.crash sealed interface CrashDetectionEvents { - object ResetAllCrashData : CrashDetectionEvents - object ResetAppHasCrashed : CrashDetectionEvents + data object ResetAllCrashData : CrashDetectionEvents + data object ResetAppHasCrashed : CrashDetectionEvents } diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/detection/RageshakeDetectionEvents.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/detection/RageshakeDetectionEvents.kt index bfba87a01a..ff587293d5 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/detection/RageshakeDetectionEvents.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/detection/RageshakeDetectionEvents.kt @@ -19,9 +19,9 @@ package io.element.android.features.rageshake.api.detection import io.element.android.features.rageshake.api.screenshot.ImageResult sealed interface RageshakeDetectionEvents { - object Dismiss : RageshakeDetectionEvents - object Disable : RageshakeDetectionEvents - object StartDetection : RageshakeDetectionEvents - object StopDetection : RageshakeDetectionEvents + data object Dismiss : RageshakeDetectionEvents + data object Disable : RageshakeDetectionEvents + data object StartDetection : RageshakeDetectionEvents + data object StopDetection : RageshakeDetectionEvents data class ProcessScreenshot(val imageResult: ImageResult) : RageshakeDetectionEvents } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt index 9765f83da0..cde53ade5a 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt @@ -17,9 +17,9 @@ package io.element.android.features.rageshake.impl.bugreport sealed interface BugReportEvents { - object SendBugReport : BugReportEvents - object ResetAll : BugReportEvents - object ClearError : BugReportEvents + data object SendBugReport : BugReportEvents + data object ResetAll : BugReportEvents + data object ClearError : BugReportEvents data class SetDescription(val description: String) : BugReportEvents data class SetSendLog(val sendLog: Boolean) : BugReportEvents diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index ad7f58a239..46ccc7fc56 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -268,12 +268,13 @@ class DefaultBugReporter @Inject constructor( } } - if (!uploadedSomeLogs) { - error("Couldn't upload any logs") - } - mBugReportFiles.addAll(gzippedFiles) + if (gzippedFiles.isNotEmpty() && !uploadedSomeLogs) { + serverError = "Couldn't upload any logs, please retry." + return@withContext + } + if (withScreenshot) { screenshotHolder.getFileUri() ?.toUri() diff --git a/features/rageshake/impl/src/main/res/values-cs/translations.xml b/features/rageshake/impl/src/main/res/values-cs/translations.xml index d95752e91c..bfea484240 100644 --- a/features/rageshake/impl/src/main/res/values-cs/translations.xml +++ b/features/rageshake/impl/src/main/res/values-cs/translations.xml @@ -1,7 +1,7 @@ "Připojit snímek obrazovky" - "V případě dalších dotazů se na mě můžete obrátit" + "V případě dalších dotazů se na mě můžete obrátit." "Kontaktujte mě" "Upravit snímek obrazovky" "Popište prosím chybu. Co jste udělali? Co jste očekávali, že se stane? Co se ve skutečnosti stalo? Uveďte co nejvíce podrobností." diff --git a/features/rageshake/impl/src/main/res/values-de/translations.xml b/features/rageshake/impl/src/main/res/values-de/translations.xml index b316d8b45e..51b331a86f 100644 --- a/features/rageshake/impl/src/main/res/values-de/translations.xml +++ b/features/rageshake/impl/src/main/res/values-de/translations.xml @@ -10,6 +10,6 @@ "Absturzprotokolle senden" "Logs zulassen" "Bildschirmfoto senden" - "Um zu überprüfen, ob alles wie vorgesehen funktioniert, werden Protokolle mit deiner Nachricht gesendet. Diese werden privat sein. Um nur Ihre Nachricht zu senden, schalte diese Einstellung aus." + "Deiner Nachricht werden Protokolle beigefügt, um sicherzustellen, dass alles ordnungsgemäß funktioniert. Um deine Nachricht ohne Logs zu senden, deaktiviere diese Einstellung." "%1$s ist bei der letzten Verwendung abgestürzt. Möchtest du uns einen Absturzbericht senden?" diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt index e73d63f38c..4fa5c18b2e 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -29,7 +29,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint { sealed interface InitialTarget : Parcelable { @Parcelize - object RoomDetails : InitialTarget + data object RoomDetails : InitialTarget @Parcelize data class RoomMemberDetails(val roomMemberId: UserId) : InitialTarget diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsAction.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsAction.kt index 61b3da21f9..bdd92fb589 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsAction.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsAction.kt @@ -17,7 +17,6 @@ package io.element.android.features.roomdetails.impl sealed interface RoomDetailsAction { - object Edit : RoomDetailsAction - - object AddTopic : RoomDetailsAction + data object Edit : RoomDetailsAction + data object AddTopic : RoomDetailsAction } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt index 7abb9d40e7..fdad01d83c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt @@ -17,8 +17,7 @@ package io.element.android.features.roomdetails.impl sealed interface RoomDetailsEvent { - object LeaveRoom : RoomDetailsEvent - object MuteNotification : RoomDetailsEvent - - object UnmuteNotification : RoomDetailsEvent + data object LeaveRoom : RoomDetailsEvent + data object MuteNotification : RoomDetailsEvent + data object UnmuteNotification : RoomDetailsEvent } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index a588c9f3a5..675ef7de60 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -56,16 +56,16 @@ class RoomDetailsFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - object RoomDetails : NavTarget + data object RoomDetails : NavTarget @Parcelize - object RoomMemberList : NavTarget + data object RoomMemberList : NavTarget @Parcelize - object RoomDetailsEdit : NavTarget + data object RoomDetailsEdit : NavTarget @Parcelize - object InviteMembers : NavTarget + data object InviteMembers : NavTarget @Parcelize object RoomNotificationSettings : NavTarget diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 21b3b0fe18..8dc6f81bf1 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -40,12 +40,12 @@ data class RoomDetailsState( ) sealed interface RoomDetailsType { - object Room : RoomDetailsType + data object Room : RoomDetailsType data class Dm(val roomMember: RoomMember) : RoomDetailsType } sealed interface RoomTopicState { - object Hidden : RoomTopicState - object CanAddTopic : RoomTopicState + data object Hidden : RoomTopicState + data object CanAddTopic : RoomTopicState data class ExistingTopic(val topic: String) : RoomTopicState } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditEvents.kt index b4bc348b8a..567e8927f8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditEvents.kt @@ -22,6 +22,6 @@ sealed interface RoomDetailsEditEvents { data class HandleAvatarAction(val action: AvatarAction) : RoomDetailsEditEvents data class UpdateRoomName(val name: String) : RoomDetailsEditEvents data class UpdateRoomTopic(val topic: String) : RoomDetailsEditEvents - object Save : RoomDetailsEditEvents - object CancelSaveChanges : RoomDetailsEditEvents + data object Save : RoomDetailsEditEvents + data object CancelSaveChanges : RoomDetailsEditEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt index c09d9a1f70..05688c6cf7 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt @@ -19,6 +19,6 @@ package io.element.android.features.roomdetails.impl.members.details sealed interface RoomMemberDetailsEvents { data class BlockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents data class UnblockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents - object ClearBlockUserError : RoomMemberDetailsEvents - object ClearConfirmationDialog : RoomMemberDetailsEvents + data object ClearBlockUserError : RoomMemberDetailsEvents + data object ClearConfirmationDialog : RoomMemberDetailsEvents } diff --git a/features/roomdetails/impl/src/main/res/values-cs/translations.xml b/features/roomdetails/impl/src/main/res/values-cs/translations.xml index 6e92891c70..90a9bb0190 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -13,7 +13,12 @@ "Nelze aktualizovat místnost" "Zprávy jsou zabezpečeny zámky. Pouze vy a příjemci máte jedinečné klíče k jejich odemčení." "Šifrování zpráv povoleno" + "Při načítání nastavení oznámení došlo k chybě." + "Ztišení této místnosti se nezdařilo, zkuste to prosím znovu." + "Nepodařilo se zrušit ztišení této místnosti, zkuste to prosím znovu." "Pozvat lidi" + "Vlastní" + "Výchozí" "Oznámení" "Název místnosti" "Sdílet místnost" diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml index 992ca94b85..8ae23e8152 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -1,18 +1,23 @@ - "1 Person" + "%1$d Person" "%1$d Personen" "Thema hinzufügen" "Bereits Mitglied" "Bereits eingeladen" "Raum bearbeiten" - "Wir konnten nicht alle Informationen für diesen Raum aktualisieren." + "Es gab einen unbekannten Fehler und die Informationen konnten nicht geändert werden." "Raum konnte nicht aktualisiert werden" "Nachrichten sind mit Schlössern gesichert. Nur du und der Empfänger haben die eindeutigen Schlüssel, um sie zu entsperren." "Nachrichtenverschlüsselung aktiviert" + "Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." + "Das Stummschalten dieses Raums ist fehlgeschlagen. Bitte versuche es erneut." + "Die Stummschaltung dieses Raums konnte nicht aufgehoben werden. Bitte versuchen Sie es erneut." "Personen einladen" + "Benutzerdefiniert" + "Standard" "Benachrichtigungen" "Raumname" "Raum teilen" @@ -20,10 +25,10 @@ "Ausstehend" "Raummitglieder" "Blockieren" - "Blockierte Benutzer können dir keine Nachrichten senden und alle Nachrichten von ihnen werden ausgeblendet. Du kannst diese Aktion jederzeit rückgängig machen." + "Blockierte Benutzer können dir keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Du kannst sie jederzeit entsperren." "Nutzer blockieren" "Blockierung aufheben" - "Wenn du den Benutzer entsperrst, kannst du wieder alle Nachrichten von ihm sehen." + "Du wirst alle ihre Nachrichten wieder sehen." "Nutzer entblockieren" "Raum verlassen" "Personen" diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt index e95b5bd60d..e377764942 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt @@ -22,9 +22,9 @@ import io.element.android.libraries.matrix.api.core.RoomId sealed interface RoomListEvents { data class UpdateFilter(val newFilter: String) : RoomListEvents data class UpdateVisibleRange(val range: IntRange) : RoomListEvents - object DismissRequestVerificationPrompt : RoomListEvents - object ToggleSearchResults : RoomListEvents + data object DismissRequestVerificationPrompt : RoomListEvents + data object ToggleSearchResults : RoomListEvents data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents - object HideContextMenu : RoomListEvents + data object HideContextMenu : RoomListEvents data class LeaveRoom(val roomId: RoomId) : RoomListEvents } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 7905b5bc61..c555afeca7 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -40,7 +40,7 @@ data class RoomListState( val eventSink: (RoomListEvents) -> Unit, ) { sealed interface ContextMenu { - object Hidden : ContextMenu + data object Hidden : ContextMenu data class Shown( val roomId: RoomId, val roomName: String, diff --git a/features/roomlist/impl/src/main/res/values-cs/translations.xml b/features/roomlist/impl/src/main/res/values-cs/translations.xml index d355d2c70c..cdf719e00f 100644 --- a/features/roomlist/impl/src/main/res/values-cs/translations.xml +++ b/features/roomlist/impl/src/main/res/values-cs/translations.xml @@ -1,7 +1,9 @@ "Vytvořte novou konverzaci nebo místnost" + "Začněte tím, že někomu pošnete zprávu." + "Zatím žádné konverzace." "Všechny chaty" "Zdá se, že používáte nové zařízení. Ověřte přihlášení, abyste měli přístup k zašifrovaným zprávám." - "Přístup k historii zpráv" + "Ověřte, že jste to vy" diff --git a/features/roomlist/impl/src/main/res/values-de/translations.xml b/features/roomlist/impl/src/main/res/values-de/translations.xml index 2ed1cd0263..49a400b138 100644 --- a/features/roomlist/impl/src/main/res/values-de/translations.xml +++ b/features/roomlist/impl/src/main/res/values-de/translations.xml @@ -1,7 +1,9 @@ "Ein neues Gespräch oder einen neuen Raum erstellen" + "Beginnen, indem du jemandem eine Nachricht sendest." + "Noch keine Chats." "Alle Chats" "Es sieht so aus, als ob du ein neues Gerät verwendest. Verifiziere, dass du es bist, um auf deine verschlüsselten Nachrichten zuzugreifen." - "Greife auf deine Nachrichten-Historie zu" + "Verifiziere, dass du es bist" diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt index 752cf942c1..248e3aec10 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt @@ -29,11 +29,11 @@ data class VerifySelfSessionState( @Stable sealed interface VerificationStep { - object Initial : VerificationStep - object Canceled : VerificationStep - object AwaitingOtherDeviceResponse : VerificationStep - object Ready : VerificationStep + data object Initial : VerificationStep + data object Canceled : VerificationStep + data object AwaitingOtherDeviceResponse : VerificationStep + data object Ready : VerificationStep data class Verifying(val emojiList: List, val state: Async) : VerificationStep - object Completed : VerificationStep + data object Completed : VerificationStep } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt index 29818197e0..ad48294e92 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt @@ -125,19 +125,19 @@ class VerifySelfSessionStateMachine @Inject constructor( sealed interface State { /** The initial state, before verification started. */ - object Initial : State + data object Initial : State /** Waiting for verification acceptance. */ - object RequestingVerification : State + data object RequestingVerification : State /** Verification request accepted. Waiting for start. */ - object VerificationRequestAccepted : State + data object VerificationRequestAccepted : State /** Waiting for SaS verification start. */ - object StartingSasVerification : State + data object StartingSasVerification : State /** A SaS verification flow has been started. */ - object SasVerificationStarted : State + data object SasVerificationStarted : State sealed class Verifying(open val emojis: List) : State { /** Verification accepted and emojis received. */ @@ -148,50 +148,50 @@ class VerifySelfSessionStateMachine @Inject constructor( } /** The verification is being canceled. */ - object Canceling : State + data object Canceling : State /** The verification has been canceled, remotely or locally. */ - object Canceled : State + data object Canceled : State /** Verification successful. */ - object Completed : State + data object Completed : State } sealed interface Event { /** Request verification. */ - object RequestVerification : Event + data object RequestVerification : Event /** The current verification request has been accepted. */ - object DidAcceptVerificationRequest : Event + data object DidAcceptVerificationRequest : Event /** Start a SaS verification flow. */ - object StartSasVerification : Event + data object StartSasVerification : Event /** Started a SaS verification flow. */ - object DidStartSasVerification : Event + data object DidStartSasVerification : Event /** Has received emojis. */ data class DidReceiveChallenge(val emojis: List) : Event /** Emojis match. */ - object AcceptChallenge : Event + data object AcceptChallenge : Event /** Emojis do not match. */ - object DeclineChallenge : Event + data object DeclineChallenge : Event /** Remote accepted challenge. */ - object DidAcceptChallenge : Event + data object DidAcceptChallenge : Event /** Request cancellation. */ - object Cancel : Event + data object Cancel : Event /** Verification cancelled. */ - object DidCancel : Event + data object DidCancel : Event /** Request failed. */ - object DidFail : Event + data object DidFail : Event /** Restart the verification flow. */ - object Restart : Event + data object Restart : Event } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt index 9c0fedada4..10e95bc09a 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt @@ -17,10 +17,10 @@ package io.element.android.features.verifysession.impl sealed interface VerifySelfSessionViewEvents { - object RequestVerification: VerifySelfSessionViewEvents - object StartSasVerification: VerifySelfSessionViewEvents - object Restart: VerifySelfSessionViewEvents - object ConfirmVerification: VerifySelfSessionViewEvents - object DeclineVerification: VerifySelfSessionViewEvents - object CancelAndClose: VerifySelfSessionViewEvents + data object RequestVerification: VerifySelfSessionViewEvents + data object StartSasVerification: VerifySelfSessionViewEvents + data object Restart: VerifySelfSessionViewEvents + data object ConfirmVerification: VerifySelfSessionViewEvents + data object DeclineVerification: VerifySelfSessionViewEvents + data object CancelAndClose: VerifySelfSessionViewEvents } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 434c014932..c8af411131 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,8 +4,8 @@ [versions] # Project android_gradle_plugin = "8.1.1" -kotlin = "1.9.0" -ksp = "1.9.0-1.0.13" +kotlin = "1.9.10" +ksp = "1.9.10-1.0.13" molecule = "1.2.0" # AndroidX @@ -23,7 +23,7 @@ browser = "1.6.0" # Compose compose_bom = "2023.08.00" -composecompiler = "1.5.1" +composecompiler = "1.5.3" # Coroutines coroutines = "1.7.3" @@ -45,7 +45,7 @@ dependencycheck = "8.4.0" dependencyanalysis = "1.21.0" stem = "2.3.0" sqldelight = "1.5.5" -telephoto = "0.5.0" +telephoto = "0.6.0-SNAPSHOT" # DI dagger = "2.47" @@ -65,7 +65,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3" kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:32.2.2" +google_firebase_bom = "com.google.firebase:firebase-bom:32.2.3" # AndroidX androidx_material = { module = "com.google.android.material:material", version.ref = "material" } @@ -159,8 +159,8 @@ vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0" telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } statemachine = "com.freeletics.flowredux:compose:1.2.0" maplibre = "org.maplibre.gl:android-sdk:10.2.0" -maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.0" -maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.0" +maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.1" +maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1" # Analytics posthog = "com.posthog.android:posthog:2.0.3" diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/hash/Hash.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/hash/Hash.kt new file mode 100644 index 0000000000..6d4cc321a0 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/hash/Hash.kt @@ -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.androidutils.hash + +import java.security.MessageDigest +import java.util.Locale + +/** + * Compute a Hash of a String, using SHA-512 algorithm. + */ +fun String.hash() = try { + val digest = MessageDigest.getInstance("SHA-512") + digest.update(toByteArray()) + digest.digest() + .joinToString("") { String.format(Locale.ROOT, "%02X", it) } + .lowercase(Locale.ROOT) +} catch (exc: Exception) { + // Should not happen, but just in case + hashCode().toString() +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt index fba6066a64..f537ddcd4b 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt @@ -25,7 +25,7 @@ class FirstThrottler(private val minimumInterval: Long = 800) { private var lastDate = 0L sealed class CanHandleResult { - object Yes : CanHandleResult() + data object Yes : CanHandleResult() data class No(val shouldWaitMillis: Long) : CanHandleResult() fun waitMillis(): Long { diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt index fe728562e9..fb7bc2836b 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt @@ -63,7 +63,7 @@ sealed interface Async { /** * Represents an uninitialized operation (i.e. yet to be run). */ - object Uninitialized : Async + data object Uninitialized : Async /** * Returns the data returned by the operation, or null otherwise. diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index 35b7a65cb4..f6eacf8746 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -43,5 +43,11 @@ android { ksp(libs.showkase.processor) kspTest(libs.showkase.processor) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt index dcd1ea11bc..5edc527821 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt @@ -105,7 +105,7 @@ sealed class ElementLogoAtomSize( val shadowColorLight: Color, val shadowRadius: Dp, ) { - object Medium : ElementLogoAtomSize( + data object Medium : ElementLogoAtomSize( outerSize = 120.dp, logoSize = 83.5.dp, cornerRadius = 33.dp, @@ -115,7 +115,7 @@ sealed class ElementLogoAtomSize( shadowRadius = 32.dp, ) - object Large : ElementLogoAtomSize( + data object Large : ElementLogoAtomSize( outerSize = 158.dp, logoSize = 110.dp, cornerRadius = 44.dp, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/SunsetPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/SunsetPage.kt new file mode 100644 index 0000000000..10a6f529af --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/SunsetPage.kt @@ -0,0 +1,147 @@ +/* + * 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.atomic.pages + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAbsoluteAlignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.R +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.text.withColoredPeriod +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun SunsetPage( + isLoading: Boolean, + title: String, + subtitle: String, + modifier: Modifier = Modifier, + overallContent: @Composable () -> Unit, +) { + ElementTheme( + darkTheme = true + ) { + Box( + modifier = modifier.fillMaxSize() + ) { + SunsetBackground() + Box( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + .padding(horizontal = 16.dp, vertical = 16.dp) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = BiasAbsoluteAlignment( + horizontalBias = 0f, + verticalBias = -0.05f + ) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp, + color = ElementTheme.colors.iconPrimary + ) + } else { + Spacer(modifier = Modifier.height(24.dp)) + } + Spacer(modifier = Modifier.height(18.dp)) + Text( + text = withColoredPeriod(title), + style = ElementTheme.typography.fontHeadingXlBold, + textAlign = TextAlign.Center, + color = ElementTheme.colors.textPrimary, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + modifier = Modifier.widthIn(max = 360.dp), + text = subtitle, + style = ElementTheme.typography.fontBodyLgRegular, + textAlign = TextAlign.Center, + color = ElementTheme.colors.textPrimary, + ) + } + } + overallContent() + } + } + } +} + +@Composable +private fun SunsetBackground( + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.3f) + .background(Color.White) + ) + Image( + modifier = Modifier + .fillMaxWidth(), + painter = painterResource(id = R.drawable.light_dark), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.7f) + .background(Color(0xFF121418)) + ) + } +} + +@DayNightPreviews +@Composable +internal fun SunsetPagePreview() = ElementPreview { + SunsetPage( + isLoading = true, + title = "Title with a green period.", + subtitle = "Subtitle", + overallContent = {} + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt index 39838a218a..17e6eec258 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -79,8 +78,8 @@ fun ClickableLinkText( @Composable fun ClickableLinkText( annotatedString: AnnotatedString, - interactionSource: MutableInteractionSource, modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, linkify: Boolean = true, linkAnnotationTag: String = LINK_TAG, onClick: () -> Unit = {}, @@ -136,7 +135,6 @@ fun ClickableLinkText( layoutResult.value = it }, inlineContent = inlineContent, - color = MaterialTheme.colorScheme.primary, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt index 20589c89ee..542697b5f0 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt @@ -88,7 +88,7 @@ fun ProgressDialog( @Immutable sealed interface ProgressDialogType { data class Determinate(val progress: Float) : ProgressDialogType - object Indeterminate : ProgressDialogType + data object Indeterminate : ProgressDialogType } @Composable diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt index 6b16ff96e7..04a872a1cf 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt @@ -59,6 +59,7 @@ fun String.toAnnotatedString(): AnnotatedString = buildAnnotatedString { * @param color the color to apply to the string * @param underline whether to underline the string * @param bold whether to bold the string + * @param tagAndLink an optional pair of tag and link to add to the styled part of the string, as StringAnnotation */ @Composable fun buildAnnotatedStringWithStyledPart( @@ -67,6 +68,7 @@ fun buildAnnotatedStringWithStyledPart( color: Color = LinkColor, underline: Boolean = true, bold: Boolean = false, + tagAndLink: Pair? = null, ) = buildAnnotatedString { val coloredPart = stringResource(coloredTextRes) val fullText = stringResource(fullTextRes, coloredPart) @@ -81,4 +83,31 @@ fun buildAnnotatedStringWithStyledPart( start = startIndex, end = startIndex + coloredPart.length, ) + if (tagAndLink != null) { + addStringAnnotation( + tag = tagAndLink.first, + annotation = tagAndLink.second, + start = startIndex, + end = startIndex + coloredPart.length + ) + } +} + +/** + * Convert a string to an [AnnotatedString] with colored end period if present. + */ +fun withColoredPeriod( + text: String, +) = buildAnnotatedString { + append(text) + if (text.endsWith(".")) { + addStyle( + style = SpanStyle( + // Light.colorGreen700 + color = Color(0xff0bc491), + ), + start = text.length - 1, + end = text.length, + ) + } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconToggleButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconToggleButton.kt new file mode 100644 index 0000000000..fd355d3860 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconToggleButton.kt @@ -0,0 +1,89 @@ +/* + * 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.theme.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.RadioButtonUnchecked +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.IconToggleButtonColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.ElementThemedPreview +import io.element.android.libraries.designsystem.preview.PreviewGroup + +@Composable +fun IconToggleButton( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: IconToggleButtonColors = IconButtonDefaults.iconToggleButtonColors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable () -> Unit +) { + androidx.compose.material3.IconToggleButton( + checked = checked, + onCheckedChange = onCheckedChange, + modifier = modifier, + enabled = enabled, + colors = colors, + interactionSource = interactionSource, + content = content, + ) +} + +@Preview(group = PreviewGroup.Toggles) +@Composable +internal fun IconToggleButtonPreview() = ElementThemedPreview(vertical = false) { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + var checked by remember { mutableStateOf(false) } + Column { + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + val icon: @Composable () -> Unit = { + Icon( + imageVector = if (checked) Icons.Default.CheckCircle else Icons.Default.RadioButtonUnchecked, + contentDescription = "IconToggleButton" + ) + } + IconToggleButton(checked = checked, enabled = true, onCheckedChange = { checked = !checked }, content = icon) + IconToggleButton(checked = checked, enabled = false, onCheckedChange = { checked = !checked }, content = icon) + } + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + val icon: @Composable () -> Unit = { + Icon( + imageVector = if (!checked) Icons.Default.CheckCircle else Icons.Default.RadioButtonUnchecked, + contentDescription = "IconToggleButton" + ) + } + IconToggleButton(checked = !checked, enabled = true, onCheckedChange = { checked = !checked }, content = icon) + IconToggleButton(checked = !checked, enabled = false, onCheckedChange = { checked = !checked }, content = icon) + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt index 6be9089d11..321dc812a9 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt @@ -119,7 +119,7 @@ fun ListItem( androidx.compose.material3.ListItem( headlineContent = decoratedHeadlineContent, - modifier = Modifier.clickable(enabled = enabled && onClick != null, onClick = onClick ?: {}).then(modifier), + modifier = if (onClick != null) Modifier.clickable(enabled = enabled, onClick = onClick).then(modifier) else modifier, overlineContent = null, supportingContent = decoratedSupportingContent, leadingContent = decoratedLeadingContent, @@ -134,9 +134,9 @@ fun ListItem( * The style to use for a [ListItem]. */ sealed interface ListItemStyle { - object Default : ListItemStyle - object Primary: ListItemStyle - object Destructive : ListItemStyle + data object Default : ListItemStyle + data object Primary: ListItemStyle + data object Destructive : ListItemStyle @Composable fun headlineColor() = when (this) { Default, Primary -> ListItemDefaultColors.headline diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt index 4269ff04f8..cab1d493fc 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle @@ -30,9 +29,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup @@ -103,17 +104,21 @@ fun ListSupportingText( * @param modifier The modifier to be applied to the text. * @param contentPadding The padding to apply to the text. Default is [ListSupportingTextDefaults.Padding.Default]. */ +@OptIn(ExperimentalTextApi::class) @Composable fun ListSupportingText( annotatedString: AnnotatedString, modifier: Modifier = Modifier, contentPadding: ListSupportingTextDefaults.Padding = ListSupportingTextDefaults.Padding.Default, ) { - Text( - text = annotatedString, - modifier = modifier.padding(contentPadding.paddingValues()), - style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.colors.textSecondary, + val style = ElementTheme.typography.fontBodySmRegular + .copy(color = ElementTheme.colors.textSecondary) + val paddedModifier = modifier.padding(contentPadding.paddingValues()) + ClickableLinkText( + annotatedString = annotatedString, + modifier = paddedModifier, + style = style, + linkify = false, ) } @@ -122,13 +127,13 @@ object ListSupportingTextDefaults { /** Specifies the padding to use for the supporting text. */ sealed interface Padding { /** No padding. */ - object None : Padding + data object None : Padding /** Default padding, it will align fine with a [ListItem] with no leading content. */ - object Default : Padding + data object Default : Padding /** It will align to a [ListItem] with an [Icon] or [Checkbox] as leading content. */ - object SmallLeadingContent : Padding + data object SmallLeadingContent : Padding /** It will align to with a [ListItem] with a [Switch] as leading content. */ - object LargeLeadingContent : Padding + data object LargeLeadingContent : Padding /** It will align to with a [ListItem] with a custom start [padding]. */ data class Custom(val padding: Dp) : Padding diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt index a8b186a6b2..9f373d1b71 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt @@ -23,7 +23,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.material3.RadioButtonColors import androidx.compose.material3.RadioButtonDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -67,14 +70,15 @@ internal fun RadioButtonPreview() = ElementThemedPreview(vertical = false) { Con @Composable private fun ContentToPreview() { + var checked by remember { mutableStateOf(false) } Column { Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - RadioButton(selected = false, onClick = {}) - RadioButton(selected = false, enabled = false, onClick = {}) + RadioButton(selected = checked, enabled = true, onClick = { checked = !checked }) + RadioButton(selected = checked, enabled = false, onClick = { checked = !checked }) } Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - RadioButton(selected = true, onClick = {}) - RadioButton(selected = true, enabled = false, onClick = {}) + RadioButton(selected = !checked, enabled = true, onClick = { checked = !checked }) + RadioButton(selected = !checked, enabled = false, onClick = { checked = !checked }) } } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt index c689d7e547..9458bf748a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt @@ -34,33 +34,40 @@ import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.components.button.ButtonVisuals import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.Snackbar +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import java.util.concurrent.atomic.AtomicBoolean /** * A global dispatcher of [SnackbarMessage] to be displayed in [Snackbar] via a [SnackbarHostState]. */ class SnackbarDispatcher { - private val mutex = Mutex() - - private val _snackbarMessage = MutableStateFlow(null) - val snackbarMessage: Flow = _snackbarMessage.asStateFlow() - - suspend fun post(message: SnackbarMessage) { - mutex.withLock { - _snackbarMessage.update { message } + private val queueMutex = Mutex() + private val snackBarMessageQueue = ArrayDeque() + val snackbarMessage: Flow = flow { + while (currentCoroutineContext().isActive) { + queueMutex.lock() + emit(snackBarMessageQueue.firstOrNull()) } } - suspend fun clear() { - mutex.withLock { - _snackbarMessage.update { null } + suspend fun post(message: SnackbarMessage) { + if (snackBarMessageQueue.isEmpty()) { + snackBarMessageQueue.add(message) + if (queueMutex.isLocked) queueMutex.unlock() + } else { + snackBarMessageQueue.add(message) + } + } + + fun clear() { + if (snackBarMessageQueue.isNotEmpty()) { + snackBarMessageQueue.removeFirstOrNull() + if (queueMutex.isLocked) queueMutex.unlock() } } } @@ -87,31 +94,51 @@ fun SnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) { } } +/** + * Helper method to display a [SnackbarMessage] in a [SnackbarHostState] handling cancellations. + */ @Composable fun rememberSnackbarHostState(snackbarMessage: SnackbarMessage?): SnackbarHostState { val snackbarHostState = remember { SnackbarHostState() } val snackbarMessageText = snackbarMessage?.let { stringResource(id = snackbarMessage.messageResId) - } + } ?: return snackbarHostState + val dispatcher = LocalSnackbarDispatcher.current - LaunchedEffect(snackbarMessage) { - if (snackbarMessageText == null) return@LaunchedEffect - launch { - snackbarHostState.showSnackbar( - message = snackbarMessageText, - duration = snackbarMessage.duration, - ) - if (isActive) { + LaunchedEffect(snackbarMessageText) { + // If the message wasn't already displayed, do it now, and mark it as displayed + // This will prevent the message from appearing in any other active SnackbarHosts + if (snackbarMessage.isDisplayed.getAndSet(true) == false) { + try { + snackbarHostState.showSnackbar( + message = snackbarMessageText, + duration = snackbarMessage.duration, + ) + // The snackbar item was displayed and dismissed, clear its message dispatcher.clear() + } catch (e: CancellationException) { + // The snackbar was being displayed when the coroutine was cancelled, + // so we need to clear its message + dispatcher.clear() + throw e } } } return snackbarHostState } +/** + * A message to be displayed in a [Snackbar]. + * @param messageResId The message to be displayed. + * @param duration The duration of the message. The default value is [SnackbarDuration.Short]. + * @param actionResId The action text to be displayed. The default value is `null`. + * @param isDisplayed Used to track if the current message is already displayed or not. + * @param action The action to be performed when the action is clicked. + */ data class SnackbarMessage( @StringRes val messageResId: Int, val duration: SnackbarDuration = SnackbarDuration.Short, @StringRes val actionResId: Int? = null, + val isDisplayed: AtomicBoolean = AtomicBoolean(false), val action: () -> Unit = {}, ) diff --git a/features/login/impl/src/main/res/drawable/light_dark.png b/libraries/designsystem/src/main/res/drawable/light_dark.png similarity index 100% rename from features/login/impl/src/main/res/drawable/light_dark.png rename to libraries/designsystem/src/main/res/drawable/light_dark.png diff --git a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcherTests.kt b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcherTests.kt new file mode 100644 index 0000000000..3eb644d800 --- /dev/null +++ b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcherTests.kt @@ -0,0 +1,91 @@ +/* + * 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.utils + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class SnackbarDispatcherTests { + + @Test + fun `given an empty queue the flow emits a null item`() = runTest { + val snackbarDispatcher = SnackbarDispatcher() + snackbarDispatcher.snackbarMessage.test { + assertThat(awaitItem()).isNull() + } + } + + @Test + fun `given an empty queue calling clear does nothing`() = runTest { + val snackbarDispatcher = SnackbarDispatcher() + snackbarDispatcher.snackbarMessage.test { + assertThat(awaitItem()).isNull() + snackbarDispatcher.clear() + expectNoEvents() + } + } + + @Test + fun `given a non-empty queue the flow emits an item`() = runTest { + val snackbarDispatcher = SnackbarDispatcher() + snackbarDispatcher.snackbarMessage.test { + snackbarDispatcher.post(SnackbarMessage(0)) + val result = expectMostRecentItem() + assertThat(result).isNotNull() + } + } + + @Test + fun `given a call to clear, the current message is cleared`() = runTest { + val snackbarDispatcher = SnackbarDispatcher() + snackbarDispatcher.snackbarMessage.test { + snackbarDispatcher.post(SnackbarMessage(0)) + val item = expectMostRecentItem() + assertThat(item).isNotNull() + snackbarDispatcher.clear() + assertThat(awaitItem()).isNull() + } + } + + @Test + fun `given 2 message emissions, the next message is displayed only after a call to clear`() = runTest { + val snackbarDispatcher = SnackbarDispatcher() + snackbarDispatcher.snackbarMessage.test { + val messageA = SnackbarMessage(0) + val messageB = SnackbarMessage(1) + + // Send message A - it is the most recent item + snackbarDispatcher.post(messageA) + assertThat(expectMostRecentItem()).isEqualTo(messageA) + + // Send message B - message A is still the most recent item + snackbarDispatcher.post(messageB) + expectNoEvents() + + // Clear the last message - message B is now the most recent item + snackbarDispatcher.clear() + assertThat(expectMostRecentItem()).isEqualTo(messageB) + + // Clear again - the queue is empty + snackbarDispatcher.clear() + assertThat(awaitItem()).isNull() + } + } + +} diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt index 65fc191e53..a0a0525fb5 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt @@ -95,7 +95,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( is StateContent -> { stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.RoomList) } - is PollContent, + is PollContent, // TODO Polls: handle last message is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> { prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisplayName, isDmRoom) } diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index dbbbc0368f..4eade84a67 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -29,7 +29,7 @@ enum class FeatureFlags( Polls( key = "feature.polls", title = "Polls", - description = "Render poll events in the timeline", + description = "Create poll and render poll events in the timeline", defaultValue = false, ), NotificationSettings( diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index a1d508885a..1ffe07eb6f 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -18,7 +18,7 @@ plugins { id("io.element.android-library") id("kotlin-parcelize") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.9.0" + kotlin("plugin.serialization") version "1.9.10" } android { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 6a76d2bf88..63093064b6 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -60,7 +60,8 @@ interface MatrixClient : Closeable { /** * Logout the user. - * Returns an optional URL. When the URL is there, it should be presented to the user after logout for RP initiated logout on their account page. + * Returns an optional URL. When the URL is there, it should be presented to the user after logout for + * Relying Party (RP) initiated logout on their account page. */ suspend fun logout(): String? suspend fun loadUserDisplayName(): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index 639509a15a..8565e4c747 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -40,54 +40,53 @@ data class NotificationData( sealed interface NotificationContent { sealed interface MessageLike : NotificationContent { - object CallAnswer : MessageLike - object CallInvite : MessageLike - object CallHangup : MessageLike - object CallCandidates : MessageLike - object KeyVerificationReady : MessageLike - object KeyVerificationStart : MessageLike - object KeyVerificationCancel : MessageLike - object KeyVerificationAccept : MessageLike - object KeyVerificationKey : MessageLike - object KeyVerificationMac : MessageLike - object KeyVerificationDone : MessageLike + data object CallAnswer : MessageLike + data object CallInvite : MessageLike + data object CallHangup : MessageLike + data object CallCandidates : MessageLike + data object KeyVerificationReady : MessageLike + data object KeyVerificationStart : MessageLike + data object KeyVerificationCancel : MessageLike + data object KeyVerificationAccept : MessageLike + data object KeyVerificationKey : MessageLike + data object KeyVerificationMac : MessageLike + data object KeyVerificationDone : MessageLike data class ReactionContent( val relatedEventId: String ) : MessageLike - object RoomEncrypted : MessageLike + data object RoomEncrypted : MessageLike data class RoomMessage( val senderId: UserId, val messageType: MessageType ) : MessageLike - object RoomRedaction : MessageLike - object Sticker : MessageLike + data object RoomRedaction : MessageLike + data object Sticker : MessageLike } sealed interface StateEvent : NotificationContent { - object PolicyRuleRoom : StateEvent - object PolicyRuleServer : StateEvent - object PolicyRuleUser : StateEvent - object RoomAliases : StateEvent - object RoomAvatar : StateEvent - object RoomCanonicalAlias : StateEvent - object RoomCreate : StateEvent - object RoomEncryption : StateEvent - object RoomGuestAccess : StateEvent - object RoomHistoryVisibility : StateEvent - object RoomJoinRules : StateEvent + data object PolicyRuleRoom : StateEvent + data object PolicyRuleServer : StateEvent + data object PolicyRuleUser : StateEvent + data object RoomAliases : StateEvent + data object RoomAvatar : StateEvent + data object RoomCanonicalAlias : StateEvent + data object RoomCreate : StateEvent + data object RoomEncryption : StateEvent + data object RoomGuestAccess : StateEvent + data object RoomHistoryVisibility : StateEvent + data object RoomJoinRules : StateEvent data class RoomMemberContent( val userId: String, val membershipState: RoomMembershipState ) : StateEvent - object RoomName : StateEvent - object RoomPinnedEvents : StateEvent - object RoomPowerLevels : StateEvent - object RoomServerAcl : StateEvent - object RoomThirdPartyInvite : StateEvent - object RoomTombstone : StateEvent - object RoomTopic : StateEvent - object SpaceChild : StateEvent - object SpaceParent : StateEvent + data object RoomName : StateEvent + data object RoomPinnedEvents : StateEvent + data object RoomPowerLevels : StateEvent + data object RoomServerAcl : StateEvent + data object RoomThirdPartyInvite : StateEvent + data object RoomTombstone : StateEvent + data object RoomTopic : StateEvent + data object SpaceChild : StateEvent + data object SpaceParent : StateEvent } - } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt index 31e28a40db..c79ab36a7b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt @@ -84,7 +84,7 @@ object PermalinkBuilder { } sealed class PermalinkBuilderError : Throwable() { - object InvalidRoomAlias : PermalinkBuilderError() - object InvalidRoomId : PermalinkBuilderError() - object InvalidUserId : PermalinkBuilderError() + data object InvalidRoomAlias : PermalinkBuilderError() + data object InvalidRoomId : PermalinkBuilderError() + data object InvalidUserId : PermalinkBuilderError() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollKind.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollKind.kt index 85bb7c0256..b78f00bc86 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollKind.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollKind.kt @@ -20,5 +20,8 @@ enum class PollKind { Disclosed, /** Results should be only revealed when the poll is ended. */ - Undisclosed + Undisclosed, } + +val PollKind.isDisclosed: Boolean + get() = this == PollKind.Disclosed diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt index 4e41fd43ba..38ce7a03d3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt @@ -17,7 +17,7 @@ package io.element.android.libraries.matrix.api.room sealed interface MatrixRoomMembersState { - object Unknown : MatrixRoomMembersState + data object Unknown : MatrixRoomMembersState data class Pending(val prevRoomMembers: List? = null) : MatrixRoomMembersState data class Error(val failure: Throwable, val prevRoomMembers: List? = null) : MatrixRoomMembersState data class Ready(val roomMembers: List) : MatrixRoomMembersState diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt index 8714bc2c5c..c3dd6330b5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt @@ -29,7 +29,7 @@ import kotlin.time.Duration */ interface RoomList { sealed class LoadingState { - object NotLoaded : LoadingState() + data object NotLoaded : LoadingState() data class Loaded(val numberOfRooms: Int) : LoadingState() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt index 99381d0e74..9ae6c22e7d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt @@ -26,10 +26,10 @@ import kotlinx.coroutines.flow.StateFlow interface RoomListService { sealed class State { - object Idle : State() - object Running : State() - object Error : State() - object Terminated : State() + data object Idle : State() + data object Running : State() + data object Error : State() + data object Terminated : State() } /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimelineItem.kt index 38974b4002..fe328a57d2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimelineItem.kt @@ -28,6 +28,6 @@ sealed interface MatrixTimelineItem { } data class Virtual(val uniqueId: Long, val virtual: VirtualTimelineItem) : MatrixTimelineItem - object Other : MatrixTimelineItem + data object Other : MatrixTimelineItem } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineException.kt index b7c155a5aa..e3970619cd 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineException.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineException.kt @@ -17,5 +17,5 @@ package io.element.android.libraries.matrix.api.timeline sealed class TimelineException : Exception() { - object CannotPaginate : TimelineException() + data object CannotPaginate : TimelineException() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt index 3316de64eb..b16e8d2694 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt @@ -35,13 +35,12 @@ data class MessageContent( val type: MessageType? ) : EventContent - sealed interface InReplyTo { /** The event details are not loaded yet. We can fetch them. */ data class NotLoaded(val eventId: EventId) : InReplyTo /** The event details are pending to be fetched. We should **not** fetch them again. */ - object Pending : InReplyTo + data object Pending : InReplyTo /** The event details are available. */ data class Ready( @@ -60,7 +59,7 @@ sealed interface InReplyTo { * If the reason for the failure is consistent on the server, we'd enter a loop * where we keep trying to fetch the same event. * */ - object Error : InReplyTo + data object Error : InReplyTo } object RedactedContent : EventContent @@ -92,7 +91,7 @@ data class UnableToDecryptContent( val sessionId: String ) : Data - object Unknown : Data + data object Unknown : Data } } @@ -205,55 +204,25 @@ enum class MembershipChange { } sealed interface OtherState { - object PolicyRuleRoom : OtherState - - object PolicyRuleServer : OtherState - - object PolicyRuleUser : OtherState - - object RoomAliases : OtherState - - data class RoomAvatar( - val url: String? - ) : OtherState - - object RoomCanonicalAlias : OtherState - - object RoomCreate : OtherState - - object RoomEncryption : OtherState - - object RoomGuestAccess : OtherState - - object RoomHistoryVisibility : OtherState - - object RoomJoinRules : OtherState - - data class RoomName( - val name: String? - ) : OtherState - - object RoomPinnedEvents : OtherState - - object RoomPowerLevels : OtherState - - object RoomServerAcl : OtherState - - data class RoomThirdPartyInvite( - val displayName: String? - ) : OtherState - - object RoomTombstone : OtherState - - data class RoomTopic( - val topic: String? - ) : OtherState - - object SpaceChild : OtherState - - object SpaceParent : OtherState - - data class Custom( - val eventType: String - ) : OtherState + data object PolicyRuleRoom : OtherState + data object PolicyRuleServer : OtherState + data object PolicyRuleUser : OtherState + data object RoomAliases : OtherState + data class RoomAvatar(val url: String?) : OtherState + data object RoomCanonicalAlias : OtherState + data object RoomCreate : OtherState + data object RoomEncryption : OtherState + data object RoomGuestAccess : OtherState + data object RoomHistoryVisibility : OtherState + data object RoomJoinRules : OtherState + data class RoomName(val name: String?) : OtherState + data object RoomPinnedEvents : OtherState + data object RoomPowerLevels : OtherState + data object RoomServerAcl : OtherState + data class RoomThirdPartyInvite(val displayName: String?) : OtherState + data object RoomTombstone : OtherState + data class RoomTopic(val topic: String?) : OtherState + data object SpaceChild : OtherState + data object SpaceParent : OtherState + data class Custom(val eventType: String) : OtherState } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt index 3e1ee55318..265be8af79 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt @@ -19,8 +19,8 @@ package io.element.android.libraries.matrix.api.timeline.item.event import io.element.android.libraries.matrix.api.core.EventId sealed interface LocalEventSendState { - object NotSentYet : LocalEventSendState - object Canceled : LocalEventSendState + data object NotSentYet : LocalEventSendState + data object Canceled : LocalEventSendState data class SendingFailed( val error: String diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt index fa22d3cf54..eddb9eb169 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt @@ -17,9 +17,9 @@ package io.element.android.libraries.matrix.api.timeline.item.event sealed interface ProfileTimelineDetails { - object Unavailable : ProfileTimelineDetails + data object Unavailable : ProfileTimelineDetails - object Pending : ProfileTimelineDetails + data object Pending : ProfileTimelineDetails data class Ready( val displayName: String?, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt index 11fd8b9c63..ae1b24c902 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt @@ -22,7 +22,7 @@ sealed interface VirtualTimelineItem { val timestamp: Long ) : VirtualTimelineItem - object ReadMarker : VirtualTimelineItem + data object ReadMarker : VirtualTimelineItem - object EncryptedHistoryBanner : VirtualTimelineItem + data object EncryptedHistoryBanner : VirtualTimelineItem } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt index 21c6954c2a..596b611296 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt @@ -60,11 +60,11 @@ enum class Target(open val filter: String) { } sealed class LogLevel(val filter: String) { - object Warn : LogLevel("warn") - object Trace : LogLevel("trace") - object Info : LogLevel("info") - object Debug : LogLevel("debug") - object Error : LogLevel("error") + data object Warn : LogLevel("warn") + data object Trace : LogLevel("trace") + data object Info : LogLevel("info") + data object Debug : LogLevel("debug") + data object Error : LogLevel("error") } object TracingFilterConfigurations { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/WriteToFilesConfiguration.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/WriteToFilesConfiguration.kt index cafa375a6a..01aeb208ca 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/WriteToFilesConfiguration.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/WriteToFilesConfiguration.kt @@ -17,6 +17,6 @@ package io.element.android.libraries.matrix.api.tracing sealed class WriteToFilesConfiguration { - object Disabled : WriteToFilesConfiguration() + data object Disabled : WriteToFilesConfiguration() data class Enabled(val directory: String, val filenamePrefix: String) : WriteToFilesConfiguration() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt index 4cb1918ce1..c463530050 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt @@ -77,35 +77,35 @@ interface SessionVerificationService { /** Verification status of the current session. */ sealed interface SessionVerifiedStatus { /** Unknown status, we couldn't read the actual value from the SDK. */ - object Unknown : SessionVerifiedStatus + data object Unknown : SessionVerifiedStatus /** Not verified session status. */ - object NotVerified : SessionVerifiedStatus + data object NotVerified : SessionVerifiedStatus /** Verified session status. */ - object Verified : SessionVerifiedStatus + data object Verified : SessionVerifiedStatus } /** States produced by the [SessionVerificationService]. */ sealed interface VerificationFlowState { /** Initial state. */ - object Initial : VerificationFlowState + data object Initial : VerificationFlowState /** Session verification request was accepted by another device. */ - object AcceptedVerificationRequest : VerificationFlowState + data object AcceptedVerificationRequest : VerificationFlowState /** Short Authentication String (SAS) verification started between the 2 devices. */ - object StartedSasVerification : VerificationFlowState + data object StartedSasVerification : VerificationFlowState /** Verification data for the SAS verification (emojis) received. */ data class ReceivedVerificationData(val emoji: List) : VerificationFlowState /** Verification completed successfully. */ - object Finished : VerificationFlowState + data object Finished : VerificationFlowState /** Verification was cancelled by either device. */ - object Canceled : VerificationFlowState + data object Canceled : VerificationFlowState /** Verification failed with an error. */ - object Failed : VerificationFlowState + data object Failed : VerificationFlowState } diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index e29d4b73db..b2a21f26df 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -17,7 +17,7 @@ plugins { id("io.element.android-library") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.9.0" + kotlin("plugin.serialization") version "1.9.10" } android { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index ed8a7614e4..cae5df4dc4 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -32,6 +32,14 @@ const val A_PASSWORD = "password" val A_USER_ID = UserId("@alice:server.org") val A_USER_ID_2 = UserId("@bob:server.org") +val A_USER_ID_3 = UserId("@carol:server.org") +val A_USER_ID_4 = UserId("@david:server.org") +val A_USER_ID_5 = UserId("@eve:server.org") +val A_USER_ID_6 = UserId("@justin:server.org") +val A_USER_ID_7 = UserId("@mallory:server.org") +val A_USER_ID_8 = UserId("@susie:server.org") +val A_USER_ID_9 = UserId("@victor:server.org") +val A_USER_ID_10 = UserId("@walter:server.org") val A_SESSION_ID: SessionId = A_USER_ID val A_SESSION_ID_2: SessionId = A_USER_ID_2 val A_SPACE_ID = SpaceId("!aSpaceId:domain") diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarAction.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarAction.kt index 0a178f2c25..5fbc1cf44d 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarAction.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarAction.kt @@ -31,7 +31,7 @@ sealed class AvatarAction( val icon: ImageVector, val destructive: Boolean = false, ) { - object TakePhoto : AvatarAction(titleResId = CommonStrings.action_take_photo, icon = Icons.Outlined.PhotoCamera) - object ChoosePhoto : AvatarAction(titleResId = CommonStrings.action_choose_photo, icon = Icons.Outlined.PhotoLibrary) - object Remove : AvatarAction(titleResId = CommonStrings.action_remove, icon = Icons.Outlined.Delete, destructive = true) + data object TakePhoto : AvatarAction(titleResId = CommonStrings.action_take_photo, icon = Icons.Outlined.PhotoCamera) + data object ChoosePhoto : AvatarAction(titleResId = CommonStrings.action_choose_photo, icon = Icons.Outlined.PhotoLibrary) + data object Remove : AvatarAction(titleResId = CommonStrings.action_remove, icon = Icons.Outlined.Delete, destructive = true) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt index f2593766bc..979d42b826 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt @@ -35,7 +35,7 @@ data class MediaRequestData( ) { sealed interface Kind { - object Content : Kind + data object Content : Kind data class File(val body: String?, val mimeType: String) : Kind data class Thumbnail(val width: Long, val height: Long) : Kind { constructor(size: Long) : this(size, size) diff --git a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt index 7c86009d34..de07450eec 100644 --- a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt +++ b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt @@ -26,14 +26,14 @@ sealed interface PickerType { fun getContract(): ActivityResultContract fun getDefaultRequest(): Input - object Image : PickerType { + data object Image : PickerType { override fun getContract() = ActivityResultContracts.PickVisualMedia() override fun getDefaultRequest(): PickVisualMediaRequest { return PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) } } - object ImageAndVideo : PickerType { + data object ImageAndVideo : PickerType { override fun getContract() = ActivityResultContracts.PickVisualMedia() override fun getDefaultRequest(): PickVisualMediaRequest { return PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo) diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt index 938072433a..ab30f67b65 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt @@ -114,7 +114,7 @@ data class ImageCompressionResult( ) sealed interface ResizeMode { - object None : ResizeMode + data object None : ResizeMode data class Approximate(val desiredWidth: Int, val desiredHeight: Int) : ResizeMode data class Strict(val maxWidth: Int, val maxHeight: Int) : ResizeMode } diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvents.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvents.kt index a0b2411459..45232a51db 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvents.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvents.kt @@ -17,6 +17,6 @@ package io.element.android.libraries.permissions.api sealed interface PermissionsEvents { - object OpenSystemDialog : PermissionsEvents - object CloseDialog : PermissionsEvents + data object OpenSystemDialog : PermissionsEvents + data object CloseDialog : PermissionsEvents } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/gateway/PushGatewayFailure.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/gateway/PushGatewayFailure.kt index 9e8acc4d8f..c7814a1796 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/gateway/PushGatewayFailure.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/gateway/PushGatewayFailure.kt @@ -17,5 +17,5 @@ package io.element.android.libraries.push.api.gateway sealed class PushGatewayFailure : Throwable(cause = null) { - object PusherRejected : PushGatewayFailure() + data object PusherRejected : PushGatewayFailure() } diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 30a686cad6..b961146e78 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -16,7 +16,7 @@ plugins { id("io.element.android-library") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.9.0" + kotlin("plugin.serialization") version "1.9.10" } android { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt index 9dd6c7d4cd..859bff17cf 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt @@ -166,6 +166,6 @@ sealed interface OneShotNotification { } sealed interface SummaryNotification { - object Removed : SummaryNotification + data object Removed : SummaryNotification data class Update(val notification: Notification) : SummaryNotification } diff --git a/libraries/push/impl/src/main/res/values-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml index a2ae094731..04c7521d9e 100644 --- a/libraries/push/impl/src/main/res/values-de/translations.xml +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -7,7 +7,7 @@ "** Senden fehlgeschlagen - bitte Raum öffnen" "Beitreten" "Ablehnen" - "Hat dich eingeladen" + "Hat dich zum Chatten eingeladen" "Neue Nachrichten" "Reagierte mit %1$s" "Als gelesen markieren" diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts index a6565c25f0..28501b2fad 100644 --- a/libraries/pushproviders/unifiedpush/build.gradle.kts +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -16,7 +16,7 @@ plugins { id("io.element.android-library") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.9.0" + kotlin("plugin.serialization") version "1.9.10" } android { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt index d42405ef9c..4877eff555 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt @@ -31,9 +31,9 @@ class RegisterUnifiedPushUseCase @Inject constructor( ) { sealed interface RegisterUnifiedPushResult { - object Success : RegisterUnifiedPushResult - object NeedToAskUserForDistributor : RegisterUnifiedPushResult - object Error : RegisterUnifiedPushResult + data object Success : RegisterUnifiedPushResult + data object NeedToAskUserForDistributor : RegisterUnifiedPushResult + data object Error : RegisterUnifiedPushResult } suspend fun execute(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String): RegisterUnifiedPushResult { diff --git a/libraries/textcomposer/src/main/res/values-cs/translations.xml b/libraries/textcomposer/src/main/res/values-cs/translations.xml index 8e0524b69a..c37f419ec4 100644 --- a/libraries/textcomposer/src/main/res/values-cs/translations.xml +++ b/libraries/textcomposer/src/main/res/values-cs/translations.xml @@ -1,5 +1,6 @@ + "Přidat přílohu" "Přepnout seznam s odrážkami" "Přepnout blok kódu" "Zpráva…" diff --git a/libraries/textcomposer/src/main/res/values-de/translations.xml b/libraries/textcomposer/src/main/res/values-de/translations.xml index a28c784792..dea872d7c0 100644 --- a/libraries/textcomposer/src/main/res/values-de/translations.xml +++ b/libraries/textcomposer/src/main/res/values-de/translations.xml @@ -1,5 +1,6 @@ + "Anhang hinzufügen" "Aufzählungsliste ein-/ausschalten" "Codeblock umschalten" "Nachricht…" diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index 9d897db9d8..1ace446132 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -24,7 +24,7 @@ "Upravit" "Povolit" "Zapomněli jste heslo?" - "Vpřed" + "Přeposlat" "Pozvat" "Pozvat přátele" "Pozvat přátele do %1$s" @@ -40,6 +40,7 @@ "Otevřít v aplikaci" "Rychlá odpověď" "Citovat" + "Reagovat" "Odstranit" "Odpovědět" "Nahlásit chybu" @@ -94,6 +95,9 @@ "Heslo" "Lidé" "Trvalý odkaz" + "Konečné hlasy: %1$s" + "Celkový počet hlasů: %1$s" + "Výsledky se zobrazí po skončení hlasování" "Zásady ochrany osobních údajů" "Reakce" "Obnovování…" @@ -140,7 +144,11 @@ "Cestování a místa" "Symboly" "Vytvoření trvalého odkazu se nezdařilo" + "%1$s nemohl načíst mapu. Zkuste to prosím později." "Načítání zpráv se nezdařilo" + "%1$s nemá přístup k vaší poloze. Zkuste to prosím později." + "%1$s nemá oprávnění k přístupu k vaší poloze. Přístup můžete povolit v Nastavení." + "%1$s nemá oprávnění k přístupu k vaší poloze. Povolit přístup níže." "Některé zprávy nebyly odeslány" "Omlouváme se, došlo k chybě" "🔐️ Připojte se ke mně na %1$s" @@ -154,6 +162,11 @@ "%1$d členové" "%1$d členů" + + "%d hlas" + "%d hlasy" + "%d hlasů" + "Zatřeste zařízením pro nahlášení chyby" "Zdá se, že jste frustrovaně třásli telefonem. Chcete otevřít obrazovku pro nahlášení chyby?" "Tato zpráva bude nahlášena správci vašeho domovského serveru. Nebude si moci přečíst žádné šifrované zprávy." @@ -165,9 +178,35 @@ "Výběr média se nezdařil, zkuste to prosím znovu." "Nahrání média se nezdařilo, zkuste to prosím znovu." "Nahrání média se nezdařilo, zkuste to prosím znovu." - "Toto je jednorázový proces, děkujeme za čekání." - "Nastavení vašeho účtu" + "Další nastavení" + "Halsové a video hovory" + "Neshoda konfigurace" + "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. + +Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. + +Pokud budete pokračovat, některá nastavení se mohou změnit." + "Přímé zprávy" + "Vlastní nastavení pro chat" + "Při aktualizaci nastavení oznámení došlo k chybě." + "Všechny zprávy" + "Pouze zmínky a klíčová slova" + "V přímých zprávách mě upozornit na" + "Ve skupinových chatech mě upozornit na" + "Povolit oznámení na tomto zařízení" + "Konfigurace nebyla opravena, zkuste to prosím znovu." + "Skupinové chaty" + "Zmínky" + "Vše" + "Zmínky" + "Upozornit mě na" + "Upozornit mě na @room" + "Chcete-li dostávat oznámení, změňte prosím svůj %1$s." + "systémová nastavení" + "Systémová oznámení byla vypnuta" + "Oznámení" "Zaškrtněte, pokud chcete skrýt všechny aktuální a budoucí zprávy od tohoto uživatele" + "Účet a zařízení" "Sdílet polohu" "Sdílet moji polohu" "Otevřít v Mapách Apple" diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 0f5259e663..00979596a9 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -8,7 +8,7 @@ "Zurück" "Abbrechen" "Foto auswählen" - "Löschen" + "Zurücksetzen" "Schließen" "Verifizierung abschließen" "Bestätigen" @@ -37,9 +37,10 @@ "Nein" "Nicht jetzt" "OK" - "Öffne mit" + "Öffnen mit" "Schnellantwort" "Zitieren" + "Reagieren" "Entfernen" "Antworten" "Fehler melden" @@ -56,7 +57,7 @@ "Starten" "Chat starten" "Verifizierung starten" - "Tippe, um die Karte zu laden" + "Zum Karte laden tippen" "Foto aufnehmen" "Quelltext anzeigen" "Ja" @@ -80,25 +81,28 @@ "Nachricht weiterleiten" "GIF" "Bild" - "Wir können die Matrix-ID dieses Benutzers nicht validieren. Die Einladung wurde möglicherweise nicht empfangen." - "Raum verlassen" + "Diese Matrix-ID kann nicht gefunden werden, daher wird die Einladung möglicherweise nicht empfangen." + "Verlasse Raum" "Link in Zwischenablage kopiert" - "Wird geladen…" + "Lädt…" "Nachricht" "Nachrichtenlayout" - "Nachricht wurde entfernt" + "Nachricht entfernt" "Modern" "Stummschalten" "Keine Ergebnisse" "Offline" "Passwort" "Personen" - "Permalink" + "Dauerlink" + "Endgültige Stimmen: %1$s" + "Stimmen insgesamt: %1$s" + "Ergebnisse werden nach Ende der Umfrage angezeigt" "Datenschutz­erklärung" "Reaktionen" "Aktualisiere…" "Auf %1$s antworten" - "Melde einen Fehler" + "Einen Fehler melden" "Bericht gesendet" "Raumname" "z.B. dein Projektname" @@ -106,12 +110,12 @@ "Suchergebnisse" "Sicherheit" "Wählen deinen Server" - "Senden…" + "Sendet…" "Server wird nicht unterstützt" "Server-URL" "Einstellungen" "Geteilter Standort" - "Chat wird gestartet…" + "Starte Chat…" "Sticker" "Erfolg" "Vorschläge" @@ -120,7 +124,7 @@ "Thema" "Worum geht es in diesem Raum?" "Entschlüsselung nicht möglich" - "Wir konnten Einladungen nicht erfolgreich an einen oder mehrere Benutzer senden." + "Einladungen konnten nicht an einen oder mehrere Benutzer gesendet werden." "Einladung(en) können nicht gesendet werden" "Stummschaltung aufheben" "Nicht unterstütztes Ereignis" @@ -128,7 +132,7 @@ "Verifizierung abgebrochen" "Verifizierung abgeschlossen" "Video" - "Warten…" + "Warte…" "Bestätigung" "Warnung" "Aktivitäten" @@ -139,10 +143,12 @@ "Smileys & Personen" "Reisen & Orte" "Symbole" - "Fehler beim Erstellen des Permalinks" + "Fehler beim Erstellen des Dauerlinks" "%1$s konnte die Karte nicht laden. Bitte versuche es später erneut." "Fehler beim Laden der Nachrichten" "%1$s konnte nicht auf deinen Standort zugreifen. Bitte versuche es später erneut." + "%1$s hat keine Berechtigung, auf deinen Standort zuzugreifen. Du kannst den Zugriff in den Einstellungen aktivieren." + "%1$s hat keine Berechtigung, auf deinen Standort zuzugreifen. Aktiviere den Zugriff unten." "Einige Nachrichten wurden nicht gesendet" "Entschuldigung, ein Fehler ist aufgetreten." "🔐️ Besuche mich auf %1$s" @@ -155,9 +161,13 @@ "%1$d Mitglied" "%1$d Mitglieder" - "Rageshake zum Melden von Fehlern" + + "%d Stimme" + "%d Stimmen" + + "Schütteln zum Melden von Fehlern" "Du scheinst frustriert das Telefon zu schütteln. Möchtest du den Fehlerberichtsbildschirm öffnen?" - "Diese Nachricht wird an deinen Heimserver-Admin gemeldet werden. Er wird nicht in der Lage sein, verschlüsselte Nachrichten zu lesen." + "Diese Nachricht wird an deinen Heimserver-Admin gemeldet. Er wird nicht in der Lage sein, verschlüsselte Nachrichten zu lesen." "Grund für die Meldung dieses Inhalts" "Dies ist der Anfang von %1$s." "Dies ist der Beginn dieser Konversation." @@ -166,9 +176,35 @@ "Medienauswahl fehlgeschlagen, bitte versuche es erneut." "Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuche es erneut." "Hochladen von Medien fehlgeschlagen, bitte versuchen Sie es erneut." - "Dies ist ein einmaliger Vorgang, danke fürs Warten." - "Dein Konto einrichten" + "Zusätzliche Einstellungen" + "Audio- und Videoanrufe" + "Konfigurationskonflikt" + "Wir haben die Benachrichtigungseinstellungen vereinfacht, damit Optionen leichter zu finden sind. + +Einige benutzerdefinierte Einstellungen, die du in der Vergangenheit ausgewählt hast, werden hier nicht angezeigt, sind aber immer noch aktiv. + +Wenn du fortfährst, ändern sich möglicherweise einige deine Einstellungen." + "Direkte Chats" + "Benutzerdefinierte Einstellung pro Chat" + "Beim Aktualisieren der Benachrichtigungseinstellung ist ein Fehler aufgetreten." + "Alle Nachrichten" + "Nur Erwähnungen und Schlüsselwörter" + "Bei direkten Chats, benachrichtigen mich für" + "Bei Gruppenchats, benachrichtigte mich für" + "Benachrichtigungen auf diesem Gerät aktivieren" + "Die Konfiguration wurde nicht korrigiert. Bitte versuche es erneut." + "Gruppenchats" + "Erwähnungen" + "Alle" + "Erwähnungen" + "Benachrichtige mich für" + "Benachrichtige mich bei @room" + "Um Benachrichtigungen zu erhalten, ändern bitte deine %1$s." + "Systemeinstellungen" + "Systembenachrichtigungen deaktiviert" + "Benachrichtigungen" "Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest" + "Konto und Geräte" "Standort teilen" "Meinen Standort teilen" "In Apple Maps öffnen" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index e3fe11dfdc..e7020bc47b 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -165,8 +165,6 @@ "Impossible de sélectionner un média, veuillez réessayer." "Échec du traitement du média avant son envoi, veuillez réessayer." "Impossible d’envoyer le média, veuillez réessayer." - "Ce processus n’a besoin d’être fait qu’une seule fois, merci de patienter." - "Configuration de votre compte." "Activer les notifications sur cet appareil" "paramètres système" "Notifications système désactivées" diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index 77c2df4268..23df74fff5 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -178,15 +178,23 @@ "Не удалось выбрать носитель, попробуйте еще раз." "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." "Не удалось загрузить медиафайлы, попробуйте еще раз." - "Это одноразовый процесс, спасибо, что подождали." - "Настройка учетной записи." "Дополнительные параметры" "Аудио и видео звонки" + "Несоответствие конфигурации" + "Мы упростили настройки уведомлений, чтобы упростить поиск опций. + +Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны. + +Если вы продолжите, некоторые настройки могут быть изменены." "Прямые чаты" + "Индивидуальные настройки для каждого чата" "При обновлении настроек уведомления произошла ошибка." + "Все сообщения" + "Только упоминания и ключевые слова" "Уведомлять меня в личных чатах" "Уведомлять меня в групповых чатах" "Включить уведомления на данном устройстве" + "Конфигурация не была исправлена, попробуйте еще раз." "Групповые чаты" "Упоминания" "Все" @@ -198,6 +206,7 @@ "Системные уведомления выключены" "Уведомления" "Отметьте, хотите ли вы скрыть все текущие и будущие сообщения от этого пользователя" + "Учетная запись и устройства" "Поделиться местоположением" "Поделиться моим местоположением" "Открыть в Apple Maps" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 14dd5626c8..b8d919faa0 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -178,14 +178,23 @@ "Nepodarilo sa vybrať médium, skúste to prosím znova." "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." "Nepodarilo sa nahrať médiá, skúste to prosím znova." - "Ide o jednorazový proces, ďakujeme za trpezlivosť." - "Nastavenie vášho účtu." "Ďalšie nastavenia" "Audio a video hovory" + "Nezhoda konfigurácie" + "Zjednodušili sme Nastavenia oznámení, aby ste ľahšie našli možnosti. + +Niektoré vlastné nastavenia, ktoré ste si nastavili v minulosti, sa tu nezobrazujú, ale sú stále aktívne. + +Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť." "Priame konverzácie" + "Vlastné nastavenie pre konverzácie" + "Pri aktualizácii nastavenia oznámenia došlo k chybe." + "Všetky správy" + "Iba zmienky a kľúčové slová" "Pri priamych rozhovoroch ma upozorniť na" "Pri skupinových rozhovoroch ma upozorniť na" "Povoliť oznámenia na tomto zariadení" + "Konfigurácia nebola opravená, skúste to prosím znova." "Skupinové rozhovory" "Zmienky" "Všetky" @@ -197,6 +206,7 @@ "Systémové oznámenia sú vypnuté" "Oznámenia" "Označte, či chcete skryť všetky aktuálne a budúce správy od tohto používateľa" + "Účet a zariadenia" "Zdieľať polohu" "Zdieľať moju polohu" "Otvoriť v Apple Maps" diff --git a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml index 27230b0e9b..32f8a23086 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml @@ -144,7 +144,6 @@ "新訊息" "分享分析數據" "無法上傳媒體檔案,請稍後再試。" - "設定您的帳號" "其他設定" "私訊" "在這個裝置上開啟通知" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index aad13c4370..d4e74d29e4 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -181,14 +181,12 @@ "Failed selecting media, please try again." "Failed processing media to upload, please try again." "Failed uploading media, please try again." - "This is a one time process, thanks for waiting." - "Setting up your account." "Additional settings" "Audio and video calls" "Configuration mismatch" - "We’ve simplified Notifications Settings to make options easier to find. + "We’ve simplified Notifications Settings to make options easier to find. -Some custom settings you’ve chosen in the past are not shown here, but they’re still active. +Some custom settings you’ve chosen in the past are not shown here, but they’re still active. If you proceed, some of your settings may change." "Direct chats" diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 658603cb56..0e9d807014 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -56,7 +56,7 @@ private const val versionMinor = 1 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -private const val versionPatch = 4 +private const val versionPatch = 6 object Versions { val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch diff --git a/features/analytics/test/build.gradle.kts b/services/analytics/test/build.gradle.kts similarity index 93% rename from features/analytics/test/build.gradle.kts rename to services/analytics/test/build.gradle.kts index 9f1796b156..521af86a61 100644 --- a/features/analytics/test/build.gradle.kts +++ b/services/analytics/test/build.gradle.kts @@ -18,7 +18,7 @@ plugins { } android { - namespace = "io.element.android.features.analytics.test" + namespace = "io.element.android.services.analytics.test" } dependencies { diff --git a/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt similarity index 97% rename from features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt rename to services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt index 9e3fe8bb59..f3b3a7fab2 100644 --- a/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt +++ b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.analytics.test +package io.element.android.services.analytics.test import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsScreen diff --git a/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt index c808ebe503..fb5fb9fd76 100644 --- a/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt +++ b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorState.kt @@ -17,13 +17,11 @@ package io.element.android.services.apperror.api sealed interface AppErrorState { - - object NoError : AppErrorState + data object NoError : AppErrorState data class Error( val title: String, val body: String, val dismiss: () -> Unit, ) : AppErrorState - } diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationState.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationState.kt index 12cd07f05e..fc51ffc038 100644 --- a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationState.kt +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationState.kt @@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId * So we assume if we don't get the same owner, we can skip the onLeaving action as we already replaced it. */ sealed class NavigationState(open val owner: String) { - object Root : NavigationState("ROOT") + data object Root : NavigationState("ROOT") data class Session( override val owner: String, diff --git a/settings.gradle.kts b/settings.gradle.kts index 751c65d388..e1894c16d7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,6 +29,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url = URI("https://oss.sonatype.org/content/repositories/snapshots/") } maven { url = URI("https://www.jitpack.io") content { diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png index 16de0b9378..48e393db15 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a08d5ff1f91c0db7c502882815d36d6a675567ebf8c3eddc0ebff431e3592e67 -size 23390 +oid sha256:b365229cac3351e4ec44979b7a22fcae090a8995e4784d9114cfd0033a242510 +size 23147 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png index 7d6d55ba11..5790bbb52e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a5300ac57d9d0137cf82af4ed4bd86c3ab7f3a70d2560954e524b7b3c120199 -size 23515 +oid sha256:5fc43706603ce52fa3495fa4e6dfa9aa684540a9dec996a5f705427d34ffb55c +size 23274 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.migration_null_MigrationView-D-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.migration_null_MigrationView-D-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..99a9e03973 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.migration_null_MigrationView-D-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c9d9aa75b2b01e9b0106377fbcd88a92fb8b4d6a34323b18264fed8f9a48cab +size 133347 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.migration_null_MigrationView-N-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.migration_null_MigrationView-N-0_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..99a9e03973 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.migration_null_MigrationView-N-0_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c9d9aa75b2b01e9b0106377fbcd88a92fb8b4d6a34323b18264fed8f9a48cab +size 133347 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-D-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-D-1_2_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-D-0_1_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-D-1_2_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-1_3_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-0_2_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-1_3_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewDark_0_null_0,NEXUS_5,1.0,en].png index ab3bc3ee7f..bc067f20c1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7aba18d2003a96a34e6eecf4fc6d0ee139fa8073549033da04eff3fa20b4c93 -size 40454 +oid sha256:a7f1d73aa1698bb02c03773ca9c3ec8494089d03c26d52b3b6f1aaa40a081528 +size 49266 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewLight_0_null_0,NEXUS_5,1.0,en].png index 87a527b090..3b94b23ab9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.changeaccountprovider_null_ChangeAccountProviderViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0422cc4e2bae580a88dacd711570e0c5ba105d5baae103313e42a07991823083 -size 42103 +oid sha256:f2154c7d196058a0a47aa46658ad9d2ea3edf6b89e5973396980178a2399179a +size 50726 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_1,NEXUS_5,1.0,en].png index e36afba0a8..3b68e95447 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64e67ea3130d0e70bdc8d0edc0bfa92763673c15b3235d4332f554a0e7a4840a -size 45553 +oid sha256:b6344d9976849f1e56782a463fcca625943a7c6adf74c598c7a58245809b9db5 +size 53642 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_1,NEXUS_5,1.0,en].png index f589e41a6d..2e35a8461c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_null_SearchAccountProviderViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f2fd6d2fccd68abec9d829ccd673b72a0f1277644a1affb09faed7be8b8a416 -size 47021 +oid sha256:fd7e6fc09011429e0fe19bff73d3c48124b1f3bed74d3cb12c9f3f0405f53980 +size 55728 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png index 533e7086c5..58ebf07374 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04978db52b7be7d8aee3ac4aad1ec89ed4f8d9436fbd1829ec60c485e3fe8639 -size 21533 +oid sha256:79aeef6875265e119c3b4b97cea4d36ba3354ae52c4b94b69bbc09461b7bc319 +size 22259 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png index 7d03ec4b37..6e91b56f10 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52ce35020e0be63a86ad8b82f04b39e27b5960f7ae26a9ac5e1158884054608e -size 19859 +oid sha256:8dafa9a97ebc77f00fdb0432c7b94272f6ea1873c3475353be47ecde95e8b057 +size 20670 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png index 0134aee79e..9bd8087f0c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f704348b48d03ce3e788e7e37298a008116c66f3ad0a074d164df4ebbb05d9d8 -size 48964 +oid sha256:15d14bf99af3cd0433870ebd6032d9bf4a45196e2ef1df7184cc55859a704dee +size 49008 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png index 0134aee79e..170f3d997a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f704348b48d03ce3e788e7e37298a008116c66f3ad0a074d164df4ebbb05d9d8 -size 48964 +oid sha256:6e47fc219bbd63b76d01a5e50c3e4c6b1b0a8b4ec40b08b13271d0b2673d8d5e +size 50932 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png index f327dc9154..cb9d6334b0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:945d8f7b4226c451c8d33f51bf051c7f08a316c786259d49475b2e2cde407bd7 -size 46415 +oid sha256:cacb91ffca97f21bbc19b29525813f58fc8017a858aa28ccd4e620b70a8cd9ca +size 46119 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png index f327dc9154..ec2787d842 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:945d8f7b4226c451c8d33f51bf051c7f08a316c786259d49475b2e2cde407bd7 -size 46415 +oid sha256:3f1c14a23ee598ece6843a68d3ca0b1d1f725f53174bd493e27c6137de70c508 +size 48296 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c1d740b8cb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f689d80043e7d5121d072b928c866a3bde0358b5d5df06e1d4f0ceeb9a11dfef +size 56344 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentNoResults-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentNoResults-D-0_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 57ddb69c7a..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentNoResults-D-0_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fb9e4bbe341a84452206a7485a477d81725b535369b1dfad3cf430548dbb21e8 -size 46450 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentNoResults-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentNoResults-N-0_1_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 7a31eae39a..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentNoResults-N-0_1_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8bfbbae8e27c4be7ea7fadeff2470773775cb476ef37b25c8f1bb8c35b5eddd9 -size 43082 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentWithResults-D-1_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentWithResults-D-1_1_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 0134aee79e..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentWithResults-D-1_1_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f704348b48d03ce3e788e7e37298a008116c66f3ad0a074d164df4ebbb05d9d8 -size 48964 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentWithResults-N-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentWithResults-N-1_2_null,NEXUS_5,1.0,en].png deleted file mode 100644 index f327dc9154..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_ActivePollContentWithResults-N-1_2_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:945d8f7b4226c451c8d33f51bf051c7f08a316c786259d49475b2e2cde407bd7 -size 46415 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a3e90a69fe --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74508ef7f77a9c8713c75586ae4d34a9daab2608dbbd2f20de3e4d4a9a9be7e9 +size 39225 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7be79c2135 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ac9e0523fc99d472a1fe8f21719e64dbb7d1ec01059dd4183aa4a152f8ead55 +size 38673 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7ee39de1a4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed68d9ebe67d5dad938a3efcd8c2b680444c650e635bdc04cfd140ba694d9f1d +size 38928 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b2d901dc0d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d5c9d2042dad75b48b61cdbae5b2425d7c935eb860df5d5c6fa3bcb327d13d1 +size 38842 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4998418006 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4ab648f5651b7457635be3eabb914be1c976154d12a163f1c1bb2cd92168824 +size 38730 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a2cd64d048 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbb713d36f8b36ce6b55f38c10323e784a80f6187e6613ded91dc531b23a7cb7 +size 36444 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2d973414d1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31735c42ea83974595544a0f03636a2337210f23e60d4b7f8e41d46ba21d483f +size 35920 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewNoResults-D-2_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewNoResults-D-2_2_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 1eef37d750..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewNoResults-D-2_2_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4c5bc5f2086bd78a364e9996f0f41526d73d69857875594f5de7ea9998333d7 -size 23388 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewNoResults-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewNoResults-N-2_3_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 95c64c4548..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewNoResults-N-2_3_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:42848910a949ec938ccc7a3547b19d1c2c81193f9a93b1db21ca77e4d9ed9663 -size 21761 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewWithResult-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewWithResult-D-3_3_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 1eef37d750..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewWithResult-D-3_3_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4c5bc5f2086bd78a364e9996f0f41526d73d69857875594f5de7ea9998333d7 -size 23388 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewWithResult-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewWithResult-N-3_4_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 95c64c4548..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerViewWithResult-N-3_4_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:42848910a949ec938ccc7a3547b19d1c2c81193f9a93b1db21ca77e4d9ed9663 -size 21761 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9bd8087f0c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15d14bf99af3cd0433870ebd6032d9bf4a45196e2ef1df7184cc55859a704dee +size 49008 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cb9d6334b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cacb91ffca97f21bbc19b29525813f58fc8017a858aa28ccd4e620b70a8cd9ca +size 46119 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f148a727ae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a9ccbbc0a208ac398f07f0070a43d0ca5ab67ef322b274b641270a9c21a4a60 +size 48972 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..df2a5c30b9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5736eb1d232b841d62f9d96070fc9b2993453652f5d7c448b6a819db9543615e +size 45756 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7319bf6694 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2264139761f7fe3977ece3c9a7d949ac51223dd209497b7b311987cba3e5a069 +size 47154 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3a129abb0b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3ff35998ce8558b5a7af84e378bee039e36099084b357fffccf329a4983b035 +size 43551 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f60c0c7a4d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b06ec4a259dfccec114689bff7d53089bb7fc64758af23372938fd83c422071 +size 35374 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c53d96b0e5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72bb304299954abac15f9487feab06649c0151c41cbcbcbf9c887417224d499b +size 39756 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cc49158545 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:155cee63d3e031c912786b95bcc8e28dc4d1b296f8f0e4c01bd96398bb2dd040 +size 40623 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9de6f34f78 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb6afbfd69bf254a2d222deb72d80c7f0bb4fc44bc5010a7e34f2b82420a423 +size 47529 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..536ea963c9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2aca4289813d6dbce44fc19bc5e0f7f1dd9e670db4e31c5be93a9d0eac125fff +size 28696 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..29d5f43751 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2b68997a63739074bbb0622c2f96af9ad2309e61b4ac246a929d3d5e0134939 +size 124111 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ce95adf2e9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f7fbf51ee1d86b1fc7cce949f88bfcb1c7ff7f700304e5a74242c4aa4965fcb +size 33455 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8e11aa2691 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c7ca3daff99c6086f114d15dbf0649a4be7ff99a5afdeb6d0e8effda383bfec +size 36968 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..127289d30b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b682b0025c98d9d37ab8dc67cbc8a637f672ae2bf9e4a26e48209188d612c0c +size 36455 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..79faef2ce6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0680506590290c4ab5ae299abc51e0806b9a1e06a647971eae7b6a2227b0aba9 +size 44631 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..43d4dbb763 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b954906d2f4f0bb97f4ae78e246caab98a4d01efe04379ccf4ad79e8ae62310 +size 27091 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4a8356b4a6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:528e3f1f4fc9b8c236427882409bc718cd6565816ed137a47ba3dd2c69e103cc +size 108555 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png index 7e92a427ab..98964b2b77 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4dd40c1eac2e5f33ce340ce69d8c0f502cd0912a0d511602e1122a6f4d8ae330 -size 25433 +oid sha256:e22dc131e8f1f7461c050e871eaa408895529976ef7445aa6faf09852370df90 +size 25176 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png index 4a8255f3ac..48fc8ae23e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76dbcb5845565774bd33a192d02a3da0216c34b17e6e1bc1f208cabef9506617 -size 26630 +oid sha256:bc234611dd3df74196467129476c69a0212a4c665f2a94797c175cbd911c2083 +size 26339 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_SunsetPage-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_SunsetPage-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9a5371804e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_SunsetPage-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50ca2afda159bdd7f57e78b04fbe3c022a9aeee1b68f72cf4d4269b3e503f0e5 +size 127335 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_SunsetPage-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_SunsetPage-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9a5371804e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_null_SunsetPage-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50ca2afda159bdd7f57e78b04fbe3c022a9aeee1b68f72cf4d4269b3e503f0e5 +size 127335 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Toggles_IconToggleButton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Toggles_IconToggleButton_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d27488dc29 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Toggles_IconToggleButton_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f9b51e2b099380112683be07ce85679436fdaa7765cc72c200cbaca1e0b843e +size 13557 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index b2568c7cd4..b991581f9b 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -118,7 +118,14 @@ { "name": ":features:ftue:impl", "includeRegex": [ - "screen_welcome_.*" + "screen_welcome_.*", + "screen_migration_.*" + ] + }, + { + "name": ":features:poll:impl", + "includeRegex": [ + "screen_create_poll_.*" ] } ]