Merge branch 'develop' of https://github.com/vector-im/element-x-android into feature/dla/emojibase_integration
This commit is contained in:
commit
59d7947cc0
2118 changed files with 11048 additions and 4029 deletions
|
|
@ -50,6 +50,5 @@ dependencies {
|
|||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
testImplementation(projects.features.analytics.impl)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ private fun AnalyticsOptInFooter(
|
|||
)
|
||||
TextButton(
|
||||
text = stringResource(id = CommonStrings.action_not_now),
|
||||
buttonSize = ButtonSize.Medium,
|
||||
size = ButtonSize.Medium,
|
||||
onClick = onTermsDeclined,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@
|
|||
<string name="screen_analytics_prompt_read_terms">"Du kannst alle unsere Nutzerbedingungen %1$s lesen."</string>
|
||||
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Du kannst dies jederzeit deaktivieren"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben "<b>"keine"</b>" Informationen an Dritte weiter"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
|
||||
<string name="screen_analytics_prompt_title">"Hilf uns, %1$s zu verbessern"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_analytics_prompt_data_usage">"Мы не будем записывать или профилировать какие-либо персональные данные"</string>
|
||||
<string name="screen_analytics_prompt_help_us_improve">"Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы."</string>
|
||||
<string name="screen_analytics_prompt_read_terms">"Вы можете ознакомиться со всеми нашими условиями %1$s."</string>
|
||||
<string name="screen_analytics_prompt_read_terms_content_link">"здесь"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Вы можете отключить эту функцию в любое время"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"Мы не будем передавать ваши данные третьим лицам"</string>
|
||||
<string name="screen_analytics_prompt_title">"Помогите улучшить %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"我們不會和第三方分享您的資料"</string>
|
||||
</resources>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,74 +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.analytics.test
|
||||
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||
import im.vector.app.features.analytics.plan.UserProperties
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class FakeAnalyticsService(
|
||||
isEnabled: Boolean = false,
|
||||
didAskUserConsent: Boolean = false
|
||||
): AnalyticsService {
|
||||
|
||||
private val isEnabledFlow = MutableStateFlow(isEnabled)
|
||||
private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
|
||||
val capturedEvents = mutableListOf<VectorAnalyticsEvent>()
|
||||
|
||||
override fun getAvailableAnalyticsProviders(): Set<AnalyticsProvider> = emptySet()
|
||||
|
||||
override fun getUserConsent(): Flow<Boolean> = isEnabledFlow
|
||||
|
||||
override suspend fun setUserConsent(userConsent: Boolean) {
|
||||
isEnabledFlow.value = userConsent
|
||||
}
|
||||
|
||||
override fun didAskUserConsent(): Flow<Boolean> = didAskUserConsentFlow
|
||||
|
||||
override suspend fun setDidAskUserConsent() {
|
||||
didAskUserConsentFlow.value = true
|
||||
}
|
||||
|
||||
override fun getAnalyticsId(): Flow<String> = MutableStateFlow("")
|
||||
|
||||
override suspend fun setAnalyticsId(analyticsId: String) {
|
||||
}
|
||||
|
||||
override suspend fun onSignOut() {
|
||||
}
|
||||
|
||||
override fun capture(event: VectorAnalyticsEvent) {
|
||||
capturedEvents += event
|
||||
}
|
||||
|
||||
override fun screen(screen: VectorAnalyticsScreen) {
|
||||
}
|
||||
|
||||
override fun updateUserProperties(userProperties: UserProperties) {
|
||||
}
|
||||
|
||||
override fun trackError(throwable: Throwable) {
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
didAskUserConsentFlow.value = false
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
|
|
@ -63,11 +63,11 @@ internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { Con
|
|||
private fun ContentToPreview() {
|
||||
Column {
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false)
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = true)
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = false)
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.UnresolvedUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
|
|
@ -59,7 +59,7 @@ internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview { Conten
|
|||
private fun ContentToPreview() {
|
||||
Column {
|
||||
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false))
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
|
@ -117,7 +117,7 @@ fun SearchUserBar(
|
|||
}
|
||||
)
|
||||
if (index < users.lastIndex) {
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -128,7 +128,7 @@ fun SearchUserBar(
|
|||
onClick = { onUserSelected(searchResult.matrixUser) }
|
||||
)
|
||||
if (index < users.lastIndex) {
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<string name="screen_create_room_action_invite_people">"Inviter des amis sur Element"</string>
|
||||
<string name="screen_create_room_add_people_title">"Inviter des personnes"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Une erreur s\'est produite lors de la création du salon"</string>
|
||||
<string name="screen_create_room_private_option_description">"Les messages dans ce salon sont chiffrés. Une fopis activé, le chiffrement ne peut pas être désactivé."</string>
|
||||
<string name="screen_create_room_private_option_description">"Les messages dans ce salon sont chiffrés. Une fois activé, le chiffrement ne peut pas être désactivé."</string>
|
||||
<string name="screen_create_room_private_option_title">"Salon privé (sur invitation uniquement)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Les messages ne sont pas chiffrés et n\'importe qui peut les lire. Vous pouvez activer le chiffrement ultérieurement."</string>
|
||||
<string name="screen_create_room_public_option_title">"Salon public (n’importe qui)"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_create_room_action_create_room">"Новая комната"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Пригласите друзей в Element"</string>
|
||||
<string name="screen_create_room_add_people_title">"Пригласить людей"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string>
|
||||
<string name="screen_create_room_private_option_description">"Сообщения в этой комнате зашифрованы. Отключить шифрование впоследствии невозможно."</string>
|
||||
<string name="screen_create_room_private_option_title">"Приватная комната (только по приглашению)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Сообщения не зашифрованы, и каждый может их прочитать. Вы можете включить шифрование позже."</string>
|
||||
<string name="screen_create_room_public_option_title">"Публичная комната (любой)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Название комнаты"</string>
|
||||
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при попытке открытия комнаты"</string>
|
||||
<string name="screen_create_room_title">"Создать комнату"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_create_room_action_create_room">"建立聊天室"</string>
|
||||
<string name="screen_create_room_action_invite_people">"邀請朋友使用 Element"</string>
|
||||
<string name="screen_create_room_add_people_title">"邀請夥伴"</string>
|
||||
<string name="screen_create_room_error_creating_room">"建立聊天室時發生錯誤"</string>
|
||||
<string name="screen_create_room_room_name_label">"聊天室名稱"</string>
|
||||
<string name="screen_create_room_topic_label">"主題(非必填)"</string>
|
||||
<string name="screen_create_room_title">"建立聊天室"</string>
|
||||
</resources>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Plugin>,
|
||||
|
|
@ -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<FtueEntryPoint.Callback>().firstOrNull()
|
||||
|
|
@ -102,6 +107,14 @@ class FtueFlowNode @AssistedInject constructor(
|
|||
NavTarget.Placeholder -> {
|
||||
createNode<PlaceholderNode>(buildContext)
|
||||
}
|
||||
NavTarget.MigrationScreen -> {
|
||||
val callback = object : MigrationScreenNode.Callback {
|
||||
override fun onMigrationFinished() {
|
||||
lifecycleScope.launch { moveToNextStep() }
|
||||
}
|
||||
}
|
||||
createNode<MigrationScreenNode>(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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class MigrationScreenNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: MigrationScreenPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onMigrationFinished()
|
||||
}
|
||||
|
||||
private fun onMigrationFinished() {
|
||||
plugins.filterIsInstance<Callback>().forEach { it.onMigrationFinished() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
MigrationScreenView(
|
||||
state,
|
||||
onMigrationFinished = ::onMigrationFinished,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MigrationScreenState> {
|
||||
@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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,16 +13,9 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.analytics.test"
|
||||
}
|
||||
package io.element.android.features.ftue.impl.migration
|
||||
|
||||
dependencies {
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(libs.coroutines.core)
|
||||
}
|
||||
data class MigrationScreenState(
|
||||
val isMigrating: Boolean
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 = {})
|
||||
}
|
||||
|
|
@ -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_"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
11
features/ftue/impl/src/main/res/values-cs/translations.xml
Normal file
11
features/ftue/impl/src/main/res/values-cs/translations.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_message">"Toto je jednorázový proces, děkujeme za čekání."</string>
|
||||
<string name="screen_migration_title">"Nastavení vašeho účtu"</string>
|
||||
<string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string>
|
||||
<string name="screen_welcome_bullet_2">"Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici."</string>
|
||||
<string name="screen_welcome_bullet_3">"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."</string>
|
||||
<string name="screen_welcome_button">"Jdeme na to!"</string>
|
||||
<string name="screen_welcome_subtitle">"Zde je to, co potřebujete vědět:"</string>
|
||||
<string name="screen_welcome_title">"Vítá vás %1$s!"</string>
|
||||
</resources>
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_welcome_bullet_1">"Anrufe, Standortfreigabe, Suche und mehr werden später in diesem Jahr hinzugefügt."</string>
|
||||
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
|
||||
<string name="screen_migration_title">"Dein Konto einrichten"</string>
|
||||
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suche und mehr werden später in diesem Jahr hinzugefügt."</string>
|
||||
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
||||
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst."</string>
|
||||
<string name="screen_welcome_button">"Los geht\'s!"</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_message">"Ce processus n’a besoin d’être fait qu’une seule fois, merci de patienter."</string>
|
||||
<string name="screen_migration_title">"Configuration de votre compte."</string>
|
||||
<string name="screen_welcome_bullet_2">"L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>
|
||||
<string name="screen_welcome_bullet_3">"Nous serions ravis d’avoir votre avis, n’hésitez pas à nous le partager via la page des paramètres."</string>
|
||||
<string name="screen_welcome_button">"C’est parti !"</string>
|
||||
|
|
|
|||
11
features/ftue/impl/src/main/res/values-ru/translations.xml
Normal file
11
features/ftue/impl/src/main/res/values-ru/translations.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_message">"Это одноразовый процесс, спасибо, что подождали."</string>
|
||||
<string name="screen_migration_title">"Настройка учетной записи."</string>
|
||||
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
||||
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
||||
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
||||
<string name="screen_welcome_button">"Поехали!"</string>
|
||||
<string name="screen_welcome_subtitle">"Вот что вам необходимо знать:"</string>
|
||||
<string name="screen_welcome_title">"Добро пожаловать в %1$s!"</string>
|
||||
</resources>
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_welcome_bullet_1">"Hovory, zdieľanie polohy, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>
|
||||
<string name="screen_migration_message">"Ide o jednorazový proces, ďakujeme za trpezlivosť."</string>
|
||||
<string name="screen_migration_title">"Nastavenie vášho účtu."</string>
|
||||
<string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>
|
||||
<string name="screen_welcome_bullet_2">"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."</string>
|
||||
<string name="screen_welcome_bullet_3">"Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení."</string>
|
||||
<string name="screen_welcome_button">"Poďme na to!"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_title">"設定您的帳號"</string>
|
||||
<string name="screen_welcome_button">"開始吧!"</string>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
|
||||
<string name="screen_migration_title">"Setting up your account."</string>
|
||||
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
||||
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms won’t be available in this update."</string>
|
||||
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
||||
|
|
|
|||
|
|
@ -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<FtueStep?>()
|
||||
|
||||
// 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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SessionId, Boolean>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
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.TopAppBar
|
||||
|
|
@ -161,7 +161,7 @@ fun InviteListContent(
|
|||
)
|
||||
|
||||
if (index != state.inviteList.lastIndex) {
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ internal fun DefaultInviteSummaryRow(
|
|||
text = stringResource(CommonStrings.action_decline),
|
||||
onClick = onDeclineClicked,
|
||||
modifier = Modifier.weight(1f),
|
||||
buttonSize = ButtonSize.Medium,
|
||||
size = ButtonSize.Medium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
|
@ -144,7 +144,7 @@ internal fun DefaultInviteSummaryRow(
|
|||
text = stringResource(CommonStrings.action_accept),
|
||||
onClick = onAcceptClicked,
|
||||
modifier = Modifier.weight(1f),
|
||||
buttonSize = ButtonSize.Medium,
|
||||
size = ButtonSize.Medium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_decline_chat_message">"Вы уверены, что хотите отклонить приглашение в %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Отклонить приглашение"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Вы уверены, что хотите отказаться от приватного общения с %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Отклонить чат"</string>
|
||||
<string name="screen_invites_empty_list">"Нет приглашений"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) пригласил вас"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s(%2$s)邀請您"</string>
|
||||
</resources>
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ fun StaticMapView(
|
|||
StaticMapPlaceholder(
|
||||
showProgress = painter.state is AsyncImagePainter.State.Loading,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(width = maxWidth, height = maxHeight),
|
||||
width = maxWidth,
|
||||
height = maxHeight,
|
||||
onLoadMapClick = { retryHash++ }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.location.api.R
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
|
|
@ -44,34 +45,34 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
internal fun StaticMapPlaceholder(
|
||||
showProgress: Boolean,
|
||||
contentDescription: String?,
|
||||
width: Dp,
|
||||
height: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
onLoadMapClick: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.size(width = width, height = height)
|
||||
.then(if (showProgress) Modifier else Modifier.clickable(onClick = onLoadMapClick))
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.blurred_map),
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
contentScale = ContentScale.FillBounds,
|
||||
modifier = Modifier.size(width = width, height = height)
|
||||
)
|
||||
if (showProgress) {
|
||||
CircularProgressIndicator()
|
||||
} else {
|
||||
Box(
|
||||
modifier = modifier.clickable(onClick = onLoadMapClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(text = stringResource(id = CommonStrings.action_static_map_load))
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(text = stringResource(id = CommonStrings.action_static_map_load))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -85,7 +86,8 @@ internal fun StaticMapPlaceholderPreview(
|
|||
StaticMapPlaceholder(
|
||||
showProgress = values,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(400.dp),
|
||||
width = 400.dp,
|
||||
height = 400.dp,
|
||||
onLoadMapClick = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl
|
||||
package io.element.android.features.location.impl.common
|
||||
|
||||
import android.Manifest
|
||||
import android.view.Gravity
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
internal fun PermissionDeniedDialog(
|
||||
onContinue: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
appName: String,
|
||||
) {
|
||||
ConfirmationDialog(
|
||||
content = stringResource(CommonStrings.error_missing_location_auth_android, appName),
|
||||
onSubmitClicked = onContinue,
|
||||
onDismiss = onDismiss,
|
||||
submitText = stringResource(CommonStrings.action_continue),
|
||||
cancelText = stringResource(CommonStrings.action_cancel),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
internal fun PermissionRationaleDialog(
|
||||
onContinue: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
appName: String,
|
||||
) {
|
||||
ConfirmationDialog(
|
||||
content = stringResource(CommonStrings.error_missing_location_rationale_android, appName),
|
||||
onSubmitClicked = onContinue,
|
||||
onDismiss = onDismiss,
|
||||
submitText = stringResource(CommonStrings.action_continue),
|
||||
cancelText = stringResource(CommonStrings.action_cancel),
|
||||
)
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl
|
||||
package io.element.android.features.location.impl.common.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
|
@ -22,7 +22,6 @@ import android.net.Uri
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.show.LocationActions
|
||||
import io.element.android.libraries.androidutils.system.openAppSettingsPage
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.show
|
||||
package io.element.android.features.location.impl.common.actions
|
||||
|
||||
import io.element.android.features.location.api.Location
|
||||
|
||||
|
|
@ -14,8 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.permissions
|
||||
package io.element.android.features.location.impl.common.permissions
|
||||
|
||||
sealed interface PermissionsEvents {
|
||||
object RequestPermissions : PermissionsEvents
|
||||
data object RequestPermissions : PermissionsEvents
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.permissions
|
||||
package io.element.android.features.location.impl.common.permissions
|
||||
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.permissions
|
||||
package io.element.android.features.location.impl.common.permissions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.permissions
|
||||
package io.element.android.features.location.impl.common.permissions
|
||||
|
||||
data class PermissionsState(
|
||||
val permissions: Permissions = Permissions.NoneGranted,
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import io.element.android.features.location.impl.MapDefaults
|
||||
import io.element.android.features.location.impl.permissions.PermissionsEvents
|
||||
import io.element.android.features.location.impl.permissions.PermissionsPresenter
|
||||
import io.element.android.features.location.impl.permissions.PermissionsState
|
||||
import io.element.android.features.location.impl.show.LocationActions
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.features.location.impl.common.actions.LocationActions
|
||||
import io.element.android.features.messages.api.MessageComposerContext
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,10 +47,11 @@ import com.mapbox.mapboxsdk.camera.CameraPosition
|
|||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
||||
import io.element.android.features.location.impl.MapDefaults
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.R
|
||||
import io.element.android.features.location.impl.common.PermissionDeniedDialog
|
||||
import io.element.android.features.location.impl.common.PermissionRationaleDialog
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
|
|
@ -232,33 +233,3 @@ internal fun SendLocationViewPreview(
|
|||
navigateUp = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PermissionRationaleDialog(
|
||||
onContinue: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
appName: String,
|
||||
) {
|
||||
ConfirmationDialog(
|
||||
content = stringResource(CommonStrings.error_missing_location_rationale_android, appName),
|
||||
onSubmitClicked = onContinue,
|
||||
onDismiss = onDismiss,
|
||||
submitText = stringResource(CommonStrings.action_continue),
|
||||
cancelText = stringResource(CommonStrings.action_cancel),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PermissionDeniedDialog(
|
||||
onContinue: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
appName: String,
|
||||
) {
|
||||
ConfirmationDialog(
|
||||
content = stringResource(CommonStrings.error_missing_location_auth_android, appName),
|
||||
onSubmitClicked = onContinue,
|
||||
onDismiss = onDismiss,
|
||||
submitText = stringResource(CommonStrings.action_continue),
|
||||
cancelText = stringResource(CommonStrings.action_cancel),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +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
|
||||
data object DismissDialog : ShowLocationEvents
|
||||
data object RequestPermissions : ShowLocationEvents
|
||||
data object OpenAppSettings : ShowLocationEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
package io.element.android.features.location.impl.show
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -25,14 +27,18 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.MapDefaults
|
||||
import io.element.android.features.location.impl.permissions.PermissionsPresenter
|
||||
import io.element.android.features.location.impl.permissions.PermissionsState
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.actions.LocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
|
||||
class ShowLocationPresenter @AssistedInject constructor(
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
private val actions: LocationActions,
|
||||
private val locationActions: LocationActions,
|
||||
private val buildMeta: BuildMeta,
|
||||
@Assisted private val location: Location,
|
||||
@Assisted private val description: String?
|
||||
) : Presenter<ShowLocationState> {
|
||||
|
|
@ -48,19 +54,47 @@ class ShowLocationPresenter @AssistedInject constructor(
|
|||
override fun present(): ShowLocationState {
|
||||
val permissionsState: PermissionsState = permissionsPresenter.present()
|
||||
var isTrackMyLocation by remember { mutableStateOf(false) }
|
||||
val appName by remember { derivedStateOf { buildMeta.applicationName } }
|
||||
var permissionDialog: ShowLocationState.Dialog by remember {
|
||||
mutableStateOf(ShowLocationState.Dialog.None)
|
||||
}
|
||||
|
||||
LaunchedEffect(permissionsState.permissions) {
|
||||
if (permissionsState.isAnyGranted) {
|
||||
permissionDialog = ShowLocationState.Dialog.None
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: ShowLocationEvents) {
|
||||
when (event) {
|
||||
ShowLocationEvents.Share -> actions.share(location, description)
|
||||
is ShowLocationEvents.TrackMyLocation -> isTrackMyLocation = event.enabled
|
||||
ShowLocationEvents.Share -> locationActions.share(location, description)
|
||||
is ShowLocationEvents.TrackMyLocation -> {
|
||||
if (event.enabled) {
|
||||
when {
|
||||
permissionsState.isAnyGranted -> isTrackMyLocation = true
|
||||
permissionsState.shouldShowRationale -> permissionDialog = ShowLocationState.Dialog.PermissionRationale
|
||||
else -> permissionDialog = ShowLocationState.Dialog.PermissionDenied
|
||||
}
|
||||
} else {
|
||||
isTrackMyLocation = false
|
||||
}
|
||||
}
|
||||
ShowLocationEvents.DismissDialog -> permissionDialog = ShowLocationState.Dialog.None
|
||||
ShowLocationEvents.OpenAppSettings -> {
|
||||
locationActions.openSettings()
|
||||
permissionDialog = ShowLocationState.Dialog.None
|
||||
}
|
||||
ShowLocationEvents.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions)
|
||||
}
|
||||
}
|
||||
|
||||
return ShowLocationState(
|
||||
permissionDialog = permissionDialog,
|
||||
location = location,
|
||||
description = description,
|
||||
hasLocationPermission = permissionsState.isAnyGranted,
|
||||
isTrackMyLocation = isTrackMyLocation,
|
||||
appName = appName,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,17 @@ package io.element.android.features.location.impl.show
|
|||
import io.element.android.features.location.api.Location
|
||||
|
||||
data class ShowLocationState(
|
||||
val permissionDialog: Dialog,
|
||||
val location: Location,
|
||||
val description: String?,
|
||||
val hasLocationPermission: Boolean,
|
||||
val isTrackMyLocation: Boolean,
|
||||
val appName: String,
|
||||
val eventSink: (ShowLocationEvents) -> Unit,
|
||||
)
|
||||
) {
|
||||
sealed interface Dialog {
|
||||
data object None : Dialog
|
||||
data object PermissionRationale : Dialog
|
||||
data object PermissionDenied : Dialog
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,50 +19,82 @@ package io.element.android.features.location.impl.show
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.location.api.Location
|
||||
|
||||
private const val APP_NAME = "ApplicationName"
|
||||
|
||||
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
||||
override val values: Sequence<ShowLocationState>
|
||||
get() = sequenceOf(
|
||||
ShowLocationState(
|
||||
ShowLocationState.Dialog.None,
|
||||
Location(1.23, 2.34, 4f),
|
||||
description = null,
|
||||
hasLocationPermission = false,
|
||||
isTrackMyLocation = false,
|
||||
appName = APP_NAME,
|
||||
eventSink = {},
|
||||
),
|
||||
ShowLocationState(
|
||||
ShowLocationState.Dialog.PermissionDenied,
|
||||
Location(1.23, 2.34, 4f),
|
||||
description = null,
|
||||
hasLocationPermission = false,
|
||||
isTrackMyLocation = false,
|
||||
appName = APP_NAME,
|
||||
eventSink = {},
|
||||
),
|
||||
ShowLocationState(
|
||||
ShowLocationState.Dialog.PermissionRationale,
|
||||
Location(1.23, 2.34, 4f),
|
||||
description = null,
|
||||
hasLocationPermission = false,
|
||||
isTrackMyLocation = false,
|
||||
appName = APP_NAME,
|
||||
eventSink = {},
|
||||
),
|
||||
ShowLocationState(
|
||||
ShowLocationState.Dialog.None,
|
||||
Location(1.23, 2.34, 4f),
|
||||
description = null,
|
||||
hasLocationPermission = true,
|
||||
isTrackMyLocation = false,
|
||||
appName = APP_NAME,
|
||||
eventSink = {},
|
||||
),
|
||||
ShowLocationState(
|
||||
ShowLocationState.Dialog.None,
|
||||
Location(1.23, 2.34, 4f),
|
||||
description = null,
|
||||
hasLocationPermission = true,
|
||||
isTrackMyLocation = true,
|
||||
appName = APP_NAME,
|
||||
eventSink = {},
|
||||
),
|
||||
ShowLocationState(
|
||||
ShowLocationState.Dialog.None,
|
||||
Location(1.23, 2.34, 4f),
|
||||
description = "My favourite place!",
|
||||
hasLocationPermission = false,
|
||||
isTrackMyLocation = false,
|
||||
appName = APP_NAME,
|
||||
eventSink = {},
|
||||
),
|
||||
ShowLocationState(
|
||||
ShowLocationState.Dialog.None,
|
||||
Location(1.23, 2.34, 4f),
|
||||
description = "For some reason I decided to to write a small essay that wraps at just two lines!",
|
||||
hasLocationPermission = false,
|
||||
isTrackMyLocation = false,
|
||||
appName = APP_NAME,
|
||||
eventSink = {},
|
||||
),
|
||||
ShowLocationState(
|
||||
ShowLocationState.Dialog.None,
|
||||
Location(1.23, 2.34, 4f),
|
||||
description = "For some reason I decided to write a small essay in the location description. " +
|
||||
"It is so long that it will wrap onto more than two lines!",
|
||||
hasLocationPermission = false,
|
||||
isTrackMyLocation = false,
|
||||
appName = APP_NAME,
|
||||
eventSink = {},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@ import androidx.compose.ui.unit.dp
|
|||
import com.mapbox.mapboxsdk.camera.CameraPosition
|
||||
import com.mapbox.mapboxsdk.geometry.LatLng
|
||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
||||
import io.element.android.features.location.impl.MapDefaults
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.PermissionDeniedDialog
|
||||
import io.element.android.features.location.impl.common.PermissionRationaleDialog
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
|
@ -70,6 +72,20 @@ fun ShowLocationView(
|
|||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
) {
|
||||
when (state.permissionDialog) {
|
||||
ShowLocationState.Dialog.None -> Unit
|
||||
ShowLocationState.Dialog.PermissionDenied -> PermissionDeniedDialog(
|
||||
onContinue = { state.eventSink(ShowLocationEvents.OpenAppSettings) },
|
||||
onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) },
|
||||
appName = state.appName,
|
||||
)
|
||||
ShowLocationState.Dialog.PermissionRationale -> PermissionRationaleDialog(
|
||||
onContinue = { state.eventSink(ShowLocationEvents.RequestPermissions) },
|
||||
onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) },
|
||||
appName = state.appName,
|
||||
)
|
||||
}
|
||||
|
||||
val cameraPositionState = rememberCameraPositionState {
|
||||
position = CameraPosition.Builder()
|
||||
.target(LatLng(state.location.lat, state.location.lon))
|
||||
|
|
@ -116,14 +132,12 @@ fun ShowLocationView(
|
|||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (state.hasLocationPermission) {
|
||||
FloatingActionButton(
|
||||
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
|
||||
) {
|
||||
when (state.isTrackMyLocation) {
|
||||
false -> Icon(imageVector = Icons.Default.LocationSearching, contentDescription = null)
|
||||
true -> Icon(imageVector = Icons.Default.MyLocation, contentDescription = null)
|
||||
}
|
||||
FloatingActionButton(
|
||||
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
|
||||
) {
|
||||
when (state.isTrackMyLocation) {
|
||||
false -> Icon(imageVector = Icons.Default.LocationSearching, contentDescription = null)
|
||||
true -> Icon(imageVector = Icons.Default.MyLocation, contentDescription = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,11 +14,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.show
|
||||
package io.element.android.features.location.impl.common.actions
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.buildUrl
|
||||
import org.junit.Test
|
||||
import java.net.URLEncoder
|
||||
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.show
|
||||
package io.element.android.features.location.impl.common.actions
|
||||
|
||||
import io.element.android.features.location.api.Location
|
||||
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.permissions
|
||||
package io.element.android.features.location.impl.common.permissions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
|
|
@ -21,19 +21,19 @@ 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.permissions.PermissionsEvents
|
||||
import io.element.android.features.location.impl.permissions.PermissionsPresenter
|
||||
import io.element.android.features.location.impl.permissions.PermissionsPresenterFake
|
||||
import io.element.android.features.location.impl.permissions.PermissionsState
|
||||
import io.element.android.features.location.impl.show.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.features.messages.test.MessageComposerContextFake
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
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
|
||||
|
|
|
|||
|
|
@ -21,9 +21,12 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.permissions.PermissionsPresenter
|
||||
import io.element.android.features.location.impl.permissions.PermissionsPresenterFake
|
||||
import io.element.android.features.location.impl.permissions.PermissionsState
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -31,19 +34,48 @@ import org.junit.Test
|
|||
class ShowLocationPresenterTest {
|
||||
|
||||
private val permissionsPresenterFake = PermissionsPresenterFake()
|
||||
private val actions = FakeLocationActions()
|
||||
private val fakeLocationActions = FakeLocationActions()
|
||||
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
||||
private val location = Location(1.23, 4.56, 7.8f)
|
||||
private val presenter = ShowLocationPresenter(
|
||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||
override fun create(permissions: List<String>): PermissionsPresenter = permissionsPresenterFake
|
||||
},
|
||||
actions,
|
||||
fakeLocationActions,
|
||||
fakeBuildMeta,
|
||||
location,
|
||||
A_DESCRIPTION,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `emits initial state with no location permission`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
)
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.location).isEqualTo(location)
|
||||
Truth.assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
|
||||
Truth.assertThat(initialState.hasLocationPermission).isEqualTo(false)
|
||||
Truth.assertThat(initialState.isTrackMyLocation).isEqualTo(false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits initial state location permission denied once`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
)
|
||||
)
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -93,8 +125,8 @@ class ShowLocationPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
initialState.eventSink(ShowLocationEvents.Share)
|
||||
|
||||
Truth.assertThat(actions.sharedLocation).isEqualTo(location)
|
||||
Truth.assertThat(actions.sharedLabel).isEqualTo(A_DESCRIPTION)
|
||||
Truth.assertThat(fakeLocationActions.sharedLocation).isEqualTo(location)
|
||||
Truth.assertThat(fakeLocationActions.sharedLabel).isEqualTo(A_DESCRIPTION)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -116,11 +148,145 @@ class ShowLocationPresenterTest {
|
|||
|
||||
Truth.assertThat(trackMyLocationState.hasLocationPermission).isEqualTo(true)
|
||||
Truth.assertThat(trackMyLocationState.isTrackMyLocation).isEqualTo(true)
|
||||
|
||||
// Swipe the map to switch mode
|
||||
initialState.eventSink(ShowLocationEvents.TrackMyLocation(false))
|
||||
val trackLocationDisabledState = awaitItem()
|
||||
Truth.assertThat(trackLocationDisabledState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
|
||||
Truth.assertThat(trackLocationDisabledState.isTrackMyLocation).isEqualTo(false)
|
||||
Truth.assertThat(trackLocationDisabledState.hasLocationPermission).isEqualTo(true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rationale dialog dismiss`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
)
|
||||
)
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
// Skip initial state
|
||||
val initialState = awaitItem()
|
||||
|
||||
// Click on the button to switch mode
|
||||
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
|
||||
val trackLocationState = awaitItem()
|
||||
Truth.assertThat(trackLocationState.permissionDialog).isEqualTo(ShowLocationState.Dialog.PermissionRationale)
|
||||
Truth.assertThat(trackLocationState.isTrackMyLocation).isEqualTo(false)
|
||||
Truth.assertThat(trackLocationState.hasLocationPermission).isEqualTo(false)
|
||||
|
||||
// Dismiss the dialog
|
||||
initialState.eventSink(ShowLocationEvents.DismissDialog)
|
||||
val dialogDismissedState = awaitItem()
|
||||
Truth.assertThat(dialogDismissedState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
|
||||
Truth.assertThat(dialogDismissedState.isTrackMyLocation).isEqualTo(false)
|
||||
Truth.assertThat(dialogDismissedState.hasLocationPermission).isEqualTo(false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rationale dialog continue`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
)
|
||||
)
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
// Skip initial state
|
||||
val initialState = awaitItem()
|
||||
|
||||
// Click on the button to switch mode
|
||||
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
|
||||
val trackLocationState = awaitItem()
|
||||
Truth.assertThat(trackLocationState.permissionDialog).isEqualTo(ShowLocationState.Dialog.PermissionRationale)
|
||||
Truth.assertThat(trackLocationState.isTrackMyLocation).isEqualTo(false)
|
||||
Truth.assertThat(trackLocationState.hasLocationPermission).isEqualTo(false)
|
||||
|
||||
// Continue the dialog sends permission request to the permissions presenter
|
||||
trackLocationState.eventSink(ShowLocationEvents.RequestPermissions)
|
||||
Truth.assertThat(permissionsPresenterFake.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `permission denied dialog dismiss`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
)
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
// Skip initial state
|
||||
val initialState = awaitItem()
|
||||
|
||||
// Click on the button to switch mode
|
||||
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
|
||||
val trackLocationState = awaitItem()
|
||||
Truth.assertThat(trackLocationState.permissionDialog).isEqualTo(ShowLocationState.Dialog.PermissionDenied)
|
||||
Truth.assertThat(trackLocationState.isTrackMyLocation).isEqualTo(false)
|
||||
Truth.assertThat(trackLocationState.hasLocationPermission).isEqualTo(false)
|
||||
|
||||
// Dismiss the dialog
|
||||
initialState.eventSink(ShowLocationEvents.DismissDialog)
|
||||
val dialogDismissedState = awaitItem()
|
||||
Truth.assertThat(dialogDismissedState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
|
||||
Truth.assertThat(dialogDismissedState.isTrackMyLocation).isEqualTo(false)
|
||||
Truth.assertThat(dialogDismissedState.hasLocationPermission).isEqualTo(false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `open settings activity`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
)
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
// Skip initial state
|
||||
val initialState = awaitItem()
|
||||
|
||||
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
|
||||
val dialogShownState = awaitItem()
|
||||
|
||||
// Open settings
|
||||
dialogShownState.eventSink(ShowLocationEvents.OpenAppSettings)
|
||||
val settingsOpenedState = awaitItem()
|
||||
|
||||
Truth.assertThat(settingsOpenedState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
|
||||
Truth.assertThat(fakeLocationActions.openSettingsInvocationsCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `application name is in state`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.appName).isEqualTo("app name")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val A_DESCRIPTION = "My happy place"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ plugins {
|
|||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.ksp)
|
||||
id("kotlin-parcelize")
|
||||
kotlin("plugin.serialization") version "1.8.22"
|
||||
kotlin("plugin.serialization") version "1.9.0"
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
|||
|
|
@ -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<LoginFlowNode.NavTarget>(
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
|||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
|
|
@ -55,7 +55,7 @@ fun AccountProviderView(
|
|||
Column(modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }) {
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -26,8 +27,11 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.login.api.oidc.OidcAction
|
||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
|
|
@ -40,7 +44,9 @@ import java.net.URL
|
|||
class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||
@Assisted private val params: Params,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
private val authenticationService: MatrixAuthenticationService
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val defaultOidcActionFlow: DefaultOidcActionFlow,
|
||||
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||
) : Presenter<ConfirmAccountProviderState> {
|
||||
|
||||
data class Params(
|
||||
|
|
@ -61,6 +67,14 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
mutableStateOf(Async.Uninitialized)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
defaultOidcActionFlow.collect { oidcAction ->
|
||||
if (oidcAction != null) {
|
||||
onOidcAction(oidcAction, loginFlowAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: ConfirmAccountProviderEvents) {
|
||||
when (event) {
|
||||
ConfirmAccountProviderEvents.Continue -> {
|
||||
|
|
@ -97,4 +111,32 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
}.getOrThrow()
|
||||
}.runCatchingUpdatingState(loginFlowAction, errorTransform = ChangeServerError::from)
|
||||
}
|
||||
|
||||
private suspend fun onOidcAction(
|
||||
oidcAction: OidcAction,
|
||||
loginFlowAction: MutableState<Async<LoginFlow>>,
|
||||
) {
|
||||
loginFlowAction.value = Async.Loading()
|
||||
when (oidcAction) {
|
||||
OidcAction.GoBack -> {
|
||||
authenticationService.cancelOidcLogin()
|
||||
.onSuccess {
|
||||
loginFlowAction.value = Async.Uninitialized
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loginFlowAction.value = Async.Failure(failure)
|
||||
}
|
||||
}
|
||||
is OidcAction.Success -> {
|
||||
authenticationService.loginWithOidc(oidcAction.url)
|
||||
.onSuccess { _ ->
|
||||
defaultLoginUserStory.setLoginFlowIsDone(true)
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loginFlowAction.value = Async.Failure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultOidcActionFlow.reset()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,6 @@ data class ConfirmAccountProviderState(
|
|||
}
|
||||
|
||||
sealed interface LoginFlow {
|
||||
object PasswordLogin : LoginFlow
|
||||
data object PasswordLogin : LoginFlow
|
||||
data class OidcFlow(val oidcDetails: OidcDetails) : LoginFlow
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,30 +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
|
||||
|
|
@ -48,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
|
||||
|
|
@ -73,12 +58,7 @@ fun WaitListView(
|
|||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
WaitListBackground()
|
||||
WaitListContent(state, onCancelClicked)
|
||||
WaitListError(state)
|
||||
}
|
||||
WaitListContent(state, onCancelClicked, modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -99,33 +79,6 @@ 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,
|
||||
|
|
@ -133,100 +86,62 @@ private fun WaitListContent(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
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 OverallContent(
|
||||
state: WaitListState,
|
||||
onCancelClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
if (state.loginAction !is Async.Success) {
|
||||
ElementTheme(darkTheme = true) {
|
||||
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 = Color.White
|
||||
)
|
||||
} 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 = Color.White,
|
||||
)
|
||||
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 = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (state.loginAction is Async.Success) {
|
||||
ElementTheme(darkTheme = true) {
|
||||
Button(
|
||||
text = stringResource(id = CommonStrings.action_continue),
|
||||
onClick = { state.eventSink.invoke(WaitListEvents.Continue) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
Button(
|
||||
text = stringResource(id = CommonStrings.action_continue),
|
||||
onClick = { state.eventSink.invoke(WaitListEvents.Continue) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
|
|||
object LoginConstants {
|
||||
const val MATRIX_ORG_URL = "matrix.org"
|
||||
|
||||
const val DEFAULT_HOMESERVER_URL = "matrix.org" // TODO Oidc "synapse-oidc.lab.element.dev"
|
||||
const val DEFAULT_HOMESERVER_URL = "matrix.org"
|
||||
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
|
||||
}
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB |
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_account_provider_change">"Změna poskytovatele účtu"</string>
|
||||
<string name="screen_account_provider_continue">"Pokračovat"</string>
|
||||
<string name="screen_account_provider_form_hint">"Adresa domovského serveru"</string>
|
||||
<string name="screen_account_provider_form_notice">"Zadejte hledaný výraz nebo adresu domény."</string>
|
||||
<string name="screen_account_provider_form_subtitle">"Vyhledejte společnost, komunitu nebo soukromý server."</string>
|
||||
|
|
@ -10,7 +9,7 @@
|
|||
<string name="screen_account_provider_signin_title">"Chystáte se přihlásit do %s"</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily."</string>
|
||||
<string name="screen_account_provider_signup_title">"Chystáte se vytvořit účet na %s"</string>
|
||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org je otevřená síť pro bezpečnou, decentralizovanou komunikaci."</string>
|
||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org je velký bezplatný server ve veřejné síti Matrix pro bezpečnou decentralizovanou komunikaci, který provozuje nadace Matrix.org."</string>
|
||||
<string name="screen_change_account_provider_other">"Jiný"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Použijte jiného poskytovatele účtu, například vlastní soukromý server nebo pracovní účet."</string>
|
||||
<string name="screen_change_account_provider_title">"Změnit poskytovatele účtu"</string>
|
||||
|
|
@ -38,6 +37,7 @@ Díky za trpělivost!"</string>
|
|||
<string name="screen_waitlist_message_success">"Vítá vás %1$s"</string>
|
||||
<string name="screen_waitlist_title">"Jste v pořadníku!"</string>
|
||||
<string name="screen_waitlist_title_success">"Jdete do toho!"</string>
|
||||
<string name="screen_account_provider_continue">"Pokračovat"</string>
|
||||
<string name="screen_change_server_submit">"Pokračovat"</string>
|
||||
<string name="screen_change_server_title">"Vyberte svůj server"</string>
|
||||
<string name="screen_login_password_hint">"Heslo"</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_account_provider_change">"Kontoanbieter wechseln"</string>
|
||||
<string name="screen_account_provider_continue">"Weiter"</string>
|
||||
<string name="screen_account_provider_form_hint">"Adresse des Homeservers"</string>
|
||||
<string name="screen_account_provider_form_notice">"Gib einen Suchbegriff oder eine Domainadresse ein."</string>
|
||||
<string name="screen_account_provider_form_subtitle">"Suche nach einem Unternehmen, einer Community oder einem privaten Server."</string>
|
||||
|
|
@ -10,7 +9,7 @@
|
|||
<string name="screen_account_provider_signin_title">"Du bist dabei dich bei %s anzumelden"</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"Hier werden deine Konversationen stattfinden — genauso wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren."</string>
|
||||
<string name="screen_account_provider_signup_title">"Du bist dabei ein Konto auf %s zu erstellen"</string>
|
||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org ist ein offenes Netzwerk für sichere, dezentralisierte Kommunikation."</string>
|
||||
<string name="screen_change_account_provider_matrix_org_subtitle">"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."</string>
|
||||
<string name="screen_change_account_provider_other">"Andere"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Arbeitskonto."</string>
|
||||
<string name="screen_change_account_provider_title">"Kontoanbieter ändern"</string>
|
||||
|
|
@ -38,6 +37,7 @@ Vielen Dank für deine Geduld!"</string>
|
|||
<string name="screen_waitlist_message_success">"Willkommen bei %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Du hast es fast geschafft!"</string>
|
||||
<string name="screen_waitlist_title_success">"Du bist dabei."</string>
|
||||
<string name="screen_account_provider_continue">"Weiter"</string>
|
||||
<string name="screen_change_server_submit">"Weiter"</string>
|
||||
<string name="screen_change_server_title">"Wählen deinen Server"</string>
|
||||
<string name="screen_login_password_hint">"Passwort"</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
<string name="screen_login_error_unsupported_authentication">"El servidor seleccionado no admite contraseñas ni inicio de sesión OIDC. Póngase en contacto con su administrador o elija otro homeserver."</string>
|
||||
<string name="screen_login_form_header">"Introduce tus datos"</string>
|
||||
<string name="screen_login_title">"¡Hola de nuevo!"</string>
|
||||
<string name="screen_account_provider_continue">"Continuar"</string>
|
||||
<string name="screen_change_server_submit">"Continuar"</string>
|
||||
<string name="screen_change_server_title">"Selecciona tu servidor"</string>
|
||||
<string name="screen_login_password_hint">"Contraseña"</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_account_provider_change">"Changer de fournisseur"</string>
|
||||
<string name="screen_account_provider_continue">"Continuer"</string>
|
||||
<string name="screen_account_provider_form_hint">"Adresse du serveur d\'accueil"</string>
|
||||
<string name="screen_account_provider_form_notice">"Entrez un mot clé de recherche ou un nom de domaine."</string>
|
||||
<string name="screen_account_provider_form_subtitle">"Rechercher une entreprise, une communauté ou un serveur privé."</string>
|
||||
|
|
@ -10,7 +9,6 @@
|
|||
<string name="screen_account_provider_signin_title">"Vous êtes sur le point de vous connecter à %s"</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"C\'est ici que vos conversations seront stockées - tout comme vous utiliseriez un fournisseur de messagerie pour conserver vos e-mails."</string>
|
||||
<string name="screen_account_provider_signup_title">"Vous êtes sur le point de créer un compte sur %s"</string>
|
||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org est un réseau ouvert pour des communications sécurisées et décentralisées."</string>
|
||||
<string name="screen_change_account_provider_other">"Autre"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Utilisez un autre fournisseur de compte, tel que votre propre serveur ou un compte professionnel."</string>
|
||||
<string name="screen_change_account_provider_title">"Changer de fournisseur"</string>
|
||||
|
|
@ -38,6 +36,7 @@ Merci de votre patience !"</string>
|
|||
<string name="screen_waitlist_message_success">"Bienvenue sur %1$s !"</string>
|
||||
<string name="screen_waitlist_title">"Vous y êtes presque."</string>
|
||||
<string name="screen_waitlist_title_success">"Vous y êtes."</string>
|
||||
<string name="screen_account_provider_continue">"Continuer"</string>
|
||||
<string name="screen_change_server_submit">"Continuer"</string>
|
||||
<string name="screen_change_server_title">"Sélectionnez votre serveur"</string>
|
||||
<string name="screen_login_password_hint">"Mot de passe"</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
<string name="screen_login_error_unsupported_authentication">"L\'homeserver selezionato non supporta la password o l\'accesso OIDC. Contatta il tuo amministratore o scegli un altro homeserver."</string>
|
||||
<string name="screen_login_form_header">"Inserisci i tuoi dati"</string>
|
||||
<string name="screen_login_title">"Bentornato!"</string>
|
||||
<string name="screen_account_provider_continue">"Continua"</string>
|
||||
<string name="screen_change_server_submit">"Continua"</string>
|
||||
<string name="screen_change_server_title">"Seleziona il tuo server"</string>
|
||||
<string name="screen_login_password_hint">"Password"</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_account_provider_change">"Schimbați furnizorul contului"</string>
|
||||
<string name="screen_account_provider_continue">"Continuați"</string>
|
||||
<string name="screen_account_provider_form_hint">"Adresa Homeserver-ului"</string>
|
||||
<string name="screen_account_provider_form_notice">"Introduceţi un termen de căutare sau o adresă de domeniu."</string>
|
||||
<string name="screen_account_provider_form_subtitle">"Căutați o companie, o comunitate sau un server privat."</string>
|
||||
|
|
@ -10,7 +9,6 @@
|
|||
<string name="screen_account_provider_signin_title">"Sunteți pe cale să vă conectați la %s"</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"Aici vor trăi conversațiile - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile."</string>
|
||||
<string name="screen_account_provider_signup_title">"Sunteți pe cale să creați un cont pe %s"</string>
|
||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org este o rețea deschisă pentru o comunicare sigură și descentralizată."</string>
|
||||
<string name="screen_change_account_provider_other">"Altul"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Utilizați un alt furnizor de cont, cum ar fi propriul server privat sau un cont de serviciu."</string>
|
||||
<string name="screen_change_account_provider_title">"Schimbați furnizorul contului"</string>
|
||||
|
|
@ -38,6 +36,7 @@ Vă mulțumim pentru răbdare!"</string>
|
|||
<string name="screen_waitlist_message_success">"Bun venit la %1$s"</string>
|
||||
<string name="screen_waitlist_title">"Sunteți pe lista de așteptare"</string>
|
||||
<string name="screen_waitlist_title_success">"Sunteți conectat!"</string>
|
||||
<string name="screen_account_provider_continue">"Continuați"</string>
|
||||
<string name="screen_change_server_submit">"Continuați"</string>
|
||||
<string name="screen_change_server_title">"Selectați serverul"</string>
|
||||
<string name="screen_login_password_hint">"Parola"</string>
|
||||
|
|
|
|||
46
features/login/impl/src/main/res/values-ru/translations.xml
Normal file
46
features/login/impl/src/main/res/values-ru/translations.xml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_account_provider_change">"Переключить аккаунт"</string>
|
||||
<string name="screen_account_provider_form_hint">"Адрес домашнего сервера"</string>
|
||||
<string name="screen_account_provider_form_notice">"Введите поисковый запрос или адрес домена."</string>
|
||||
<string name="screen_account_provider_form_subtitle">"Поиск компании, сообщества или частного сервера."</string>
|
||||
<string name="screen_account_provider_form_title">"Поиск сервера учетной записи"</string>
|
||||
<string name="screen_account_provider_signin_subtitle">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string>
|
||||
<string name="screen_account_provider_signin_title">"Вы собираетесь войти в %s"</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string>
|
||||
<string name="screen_account_provider_signup_title">"Вы собираетесь создать учетную запись на %s"</string>
|
||||
<string name="screen_change_account_provider_other">"Другое"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись."</string>
|
||||
<string name="screen_change_account_provider_title">"Сменить поставщика учетной записи"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Нам не удалось связаться с этим домашним сервером. Убедитесь, что вы правильно ввели URL-адрес домашнего сервера. Если URL-адрес указан правильно, обратитесь к администратору домашнего сервера за дополнительной помощью."</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"В настоящее время этот сервер не поддерживает скользящую синхронизацию."</string>
|
||||
<string name="screen_change_server_form_header">"URL-адрес домашнего сервера"</string>
|
||||
<string name="screen_change_server_form_notice">"Вы можете подключиться только к существующему серверу, поддерживающему скользящую синхронизацию. Администратору домашнего сервера потребуется настроить его. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Какой адрес у вашего сервера?"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Данная учетная запись была деактивирована."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Неверное имя пользователя и/или пароль"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Это не корректный идентификатор пользователя. Ожидаемый формат: \'@user:homeserver.org\'"</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"Выбранный домашний сервер не поддерживает пароль или логин OIDC. Пожалуйста, свяжитесь с администратором или выберите другой домашний сервер."</string>
|
||||
<string name="screen_login_form_header">"Введите сведения о себе"</string>
|
||||
<string name="screen_login_title">"Рады видеть вас снова!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Войти в %1$s"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Сменить учетную запись"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Частный сервер для сотрудников Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
|
||||
<string name="screen_server_confirmation_message_register">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Вы собираетесь войти в %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Вы собираетесь создать учетную запись на %1$s"</string>
|
||||
<string name="screen_waitlist_message">"В настоящее время существует высокий спрос на %1$s на %2$s. Вернитесь в приложение через несколько дней и попробуйте снова.
|
||||
|
||||
Спасибо за терпение!"</string>
|
||||
<string name="screen_waitlist_message_success">"Добро пожаловать в %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Почти готово!"</string>
|
||||
<string name="screen_waitlist_title_success">"Вы зарегистрированы!"</string>
|
||||
<string name="screen_account_provider_continue">"Продолжить"</string>
|
||||
<string name="screen_change_server_submit">"Продолжить"</string>
|
||||
<string name="screen_change_server_title">"Выберите свой сервер"</string>
|
||||
<string name="screen_login_password_hint">"Пароль"</string>
|
||||
<string name="screen_login_submit">"Продолжить"</string>
|
||||
<string name="screen_login_subtitle">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
|
||||
<string name="screen_login_username_hint">"Имя пользователя"</string>
|
||||
</resources>
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_account_provider_change">"Zmeniť poskytovateľa účtu"</string>
|
||||
<string name="screen_account_provider_continue">"Pokračovať"</string>
|
||||
<string name="screen_account_provider_form_hint">"Adresa domovského servera"</string>
|
||||
<string name="screen_account_provider_form_notice">"Zadajte hľadaný výraz alebo adresu domény."</string>
|
||||
<string name="screen_account_provider_form_subtitle">"Vyhľadať spoločnosť, komunitu alebo súkromný server."</string>
|
||||
|
|
@ -10,7 +9,6 @@
|
|||
<string name="screen_account_provider_signin_title">"Chystáte sa prihlásiť do %s"</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"Tu budú žiť vaše konverzácie — podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."</string>
|
||||
<string name="screen_account_provider_signup_title">"Chystáte sa vytvoriť účet na %s"</string>
|
||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."</string>
|
||||
<string name="screen_change_account_provider_other">"Iný"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Použite iného poskytovateľa účtu, ako napríklad vlastný súkromný server alebo pracovný účet."</string>
|
||||
<string name="screen_change_account_provider_title">"Zmeniť poskytovateľa účtu"</string>
|
||||
|
|
@ -38,6 +36,7 @@
|
|||
<string name="screen_waitlist_message_success">"Vitajte v %1$s"</string>
|
||||
<string name="screen_waitlist_title">"Ste na čakanej listine!"</string>
|
||||
<string name="screen_waitlist_title_success">"Ste dnu!"</string>
|
||||
<string name="screen_account_provider_continue">"Pokračovať"</string>
|
||||
<string name="screen_change_server_submit">"Pokračovať"</string>
|
||||
<string name="screen_change_server_title">"Vyberte svoj server"</string>
|
||||
<string name="screen_login_password_hint">"Heslo"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_account_provider_signin_title">"您即將登入%s"</string>
|
||||
<string name="screen_account_provider_signup_title">"您即將在 %s 建立帳號"</string>
|
||||
<string name="screen_change_account_provider_other">"其他"</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"此伺服器目前不支援 sliding sync。"</string>
|
||||
<string name="screen_change_server_form_header">"家伺服器 URL"</string>
|
||||
<string name="screen_login_title">"歡迎回來!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"登入 %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_login">"您即將登入 %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"您即將在 %1$s 建立帳號"</string>
|
||||
<string name="screen_waitlist_message_success">"歡迎使用 %1$s!"</string>
|
||||
<string name="screen_account_provider_continue">"繼續"</string>
|
||||
<string name="screen_change_server_submit">"繼續"</string>
|
||||
<string name="screen_change_server_title">"選擇您的伺服器"</string>
|
||||
<string name="screen_login_password_hint">"密碼"</string>
|
||||
<string name="screen_login_submit">"繼續"</string>
|
||||
<string name="screen_login_username_hint">"使用者名稱"</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue