Merge branch 'develop' into feature/fga/user_moderation_bottomsheet

This commit is contained in:
ganfra
2025-06-03 10:46:12 +02:00
647 changed files with 7223 additions and 2888 deletions

View File

@@ -354,3 +354,12 @@ fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) {
}
}
}
configurations.all {
resolutionStrategy {
dependencySubstitution {
val tink = libs.google.tink.get()
substitute(module("com.google.crypto.tink:tink")).using(module("${tink.group}:${tink.name}:${tink.version}"))
}
}
}

View File

@@ -91,7 +91,7 @@
</intent-filter>
<!--
Element mobile links
Example: https://mobile.element.io/element?account_provider=example.org&login_hint=mxid:@alice:example.org
Example: https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
@@ -102,7 +102,7 @@
<data android:scheme="https" />
<!-- Matching asset file: https://mobile.element.io/.well-known/assetlinks.json -->
<data android:host="mobile.element.io" />
<data android:path="/element" />
<data android:path="/element/" />
</intent-filter>
<!--
matrix.to links

View File

@@ -9,4 +9,8 @@
-->
<full-backup-content>
<exclude domain="root" path="." />
<exclude domain="file" path="." />
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="external" path="."/>
</full-backup-content>

View File

@@ -10,8 +10,17 @@
<data-extraction-rules>
<cloud-backup>
<exclude domain="root" path="."/>
<exclude domain="file" path="."/>
<exclude domain="database" path="."/>
<exclude domain="sharedpref" path="."/>
<exclude domain="external" path="."/>
</cloud-backup>
<device-transfer>
<exclude domain="root" path="."/>
<exclude domain="file" path="."/>
<exclude domain="database" path="."/>
<exclude domain="sharedpref" path="."/>
<exclude domain="external" path="."/>
</device-transfer>
</data-extraction-rules>

View File

@@ -30,6 +30,7 @@
<locale android:name="sv"/>
<locale android:name="tr"/>
<locale android:name="uk"/>
<locale android:name="ur"/>
<locale android:name="uz"/>
<locale android:name="zh-CN"/>
<locale android:name="zh-TW"/>

View File

@@ -55,15 +55,13 @@ dependencies {
testImplementation(libs.test.turbine)
testImplementation(projects.features.login.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.impl)
testImplementation(projects.libraries.oidc.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.login.impl)
testImplementation(projects.tests.testutils)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.rageshake.impl)
testImplementation(projects.features.share.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test)

View File

@@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -51,6 +52,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
private val appNavigationStateService: AppNavigationStateService,
private val appCoroutineScope: CoroutineScope,
private val matrixClient: MatrixClient,
private val activeRoomsHolder: ActiveRoomsHolder,
roomComponentFactory: RoomComponentFactory,
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
backstack = BackStack(
@@ -85,6 +87,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
onCreate = {
Timber.v("OnCreate => ${inputs.room.roomId}")
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
activeRoomsHolder.addRoom(inputs.room)
fetchRoomMembers()
trackVisitedRoom()
},
@@ -95,6 +98,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
},
onDestroy = {
Timber.v("OnDestroy")
activeRoomsHolder.removeRoom(inputs.room.sessionId, inputs.room.roomId)
inputs.room.destroy()
appNavigationStateService.onLeavingRoom(id)
}

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Amaitu saioa eta bertsio-berritu"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s(e)k ez da bateragarria lehengo protokoloarekin. Amaitu saioa eta hasi berriro aplikazioa erabiltzen jarraitzeko."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Zure zerbitzaria ez da bateragarria protokolo zaharrarekin. Amaitu saioa eta hasi berriro aplikazioa erabiltzen jarraitzeko."</string>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Sair e atualizar"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s não suporta mais o protocolo antigo. Termine sessão e volte a iniciar sessão para continuar a utilizar a aplicação."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Seu servidor doméstico não é mais compatível com o protocolo antigo. Faça logout e login novamente para continuar usando o aplicativo."</string>
</resources>

View File

@@ -24,16 +24,18 @@ import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.libraries.architecture.childNode
import io.element.android.libraries.matrix.api.room.JoinedRoom
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.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class JoinBaseRoomLoadedFlowNodeTest {
class JoinedRoomLoadedFlowNodeTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@@ -96,20 +98,21 @@ class JoinBaseRoomLoadedFlowNodeTest {
}
}
private fun createJoinedRoomLoadedFlowNode(
private fun TestScope.createJoinedRoomLoadedFlowNode(
plugins: List<Plugin>,
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
coroutineScope: CoroutineScope,
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
) = JoinedRoomLoadedFlowNode(
buildContext = BuildContext.root(savedStateMap = null),
plugins = plugins,
messagesEntryPoint = messagesEntryPoint,
roomDetailsEntryPoint = roomDetailsEntryPoint,
appNavigationStateService = FakeAppNavigationStateService(),
appCoroutineScope = coroutineScope,
appCoroutineScope = this,
roomComponentFactory = FakeRoomComponentFactory(),
matrixClient = FakeMatrixClient(),
activeRoomsHolder = activeRoomsHolder,
)
@Test
@@ -121,7 +124,6 @@ class JoinBaseRoomLoadedFlowNodeTest {
val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs),
messagesEntryPoint = fakeMessagesEntryPoint,
coroutineScope = this
)
// WHEN
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
@@ -144,7 +146,6 @@ class JoinBaseRoomLoadedFlowNodeTest {
plugins = listOf(inputs),
messagesEntryPoint = fakeMessagesEntryPoint,
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
coroutineScope = this
)
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
// WHEN
@@ -154,4 +155,53 @@ class JoinBaseRoomLoadedFlowNodeTest {
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
}
@Test
fun `the ActiveRoomsHolder will be updated with the loaded room on create`() = runTest {
// GIVEN
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
val activeRoomsHolder = ActiveRoomsHolder()
val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs),
messagesEntryPoint = fakeMessagesEntryPoint,
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
activeRoomsHolder = activeRoomsHolder,
)
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
// WHEN
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.CREATED)
// THEN
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNotNull()
}
@Test
fun `the ActiveRoomsHolder will be removed on destroy`() = runTest {
// GIVEN
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
val activeRoomsHolder = ActiveRoomsHolder().apply {
addRoom(room)
}
val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs),
messagesEntryPoint = fakeMessagesEntryPoint,
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
activeRoomsHolder = activeRoomsHolder,
)
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.CREATED)
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNotNull()
// WHEN
roomFlowNode.updateLifecycleState(Lifecycle.State.DESTROYED)
// THEN
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.DESTROYED)
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
}
}

View File

@@ -22,13 +22,10 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.impl.DefaultOidcIntentResolver
import io.element.android.libraries.oidc.impl.DefaultOidcUrlParser
import io.element.android.libraries.oidc.test.FakeOidcIntentResolver
import io.element.android.tests.testutils.lambda.lambdaError
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@@ -118,8 +115,10 @@ class IntentResolverTest {
}
@Test
fun `test resolve oidc go back`() {
val sut = createIntentResolver()
fun `test resolve oidc`() {
val sut = createIntentResolver(
oidcIntentResolverResult = { OidcAction.GoBack },
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO".toUri()
@@ -132,35 +131,6 @@ class IntentResolverTest {
)
}
@Test
fun `test resolve oidc success`() {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
ResolvedIntent.Oidc(
oidcAction = OidcAction.Success(
url = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
)
)
)
}
@Test
fun `test resolve oidc invalid`() {
val sut = createIntentResolver()
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "io.element.android:/invalid".toUri()
}
assertThrows(IllegalStateException::class.java) {
sut.resolve(intent)
}
}
@Test
fun `test resolve external permalink`() {
val permalinkData = PermalinkData.UserLink(
@@ -168,7 +138,8 @@ class IntentResolverTest {
)
val sut = createIntentResolver(
loginIntentResolverResult = { null },
permalinkParserResult = { permalinkData }
permalinkParserResult = { permalinkData },
oidcIntentResolverResult = { null },
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
@@ -187,6 +158,7 @@ class IntentResolverTest {
val sut = createIntentResolver(
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) },
loginIntentResolverResult = { null },
oidcIntentResolverResult = { null },
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
@@ -202,7 +174,8 @@ class IntentResolverTest {
userId = UserId("@alice:matrix.org")
)
val sut = createIntentResolver(
permalinkParserResult = { permalinkData }
permalinkParserResult = { permalinkData },
oidcIntentResolverResult = { null },
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_BATTERY_LOW
@@ -214,7 +187,9 @@ class IntentResolverTest {
@Test
fun `test incoming share simple`() {
val sut = createIntentResolver()
val sut = createIntentResolver(
oidcIntentResolverResult = { null },
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_SEND
}
@@ -224,7 +199,9 @@ class IntentResolverTest {
@Test
fun `test incoming share multiple`() {
val sut = createIntentResolver()
val sut = createIntentResolver(
oidcIntentResolverResult = { null },
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_SEND_MULTIPLE
}
@@ -237,6 +214,7 @@ class IntentResolverTest {
val sut = createIntentResolver(
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) },
loginIntentResolverResult = { null },
oidcIntentResolverResult = { null },
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
@@ -251,6 +229,7 @@ class IntentResolverTest {
val aLoginParams = LoginParams("accountProvider", null)
val sut = createIntentResolver(
loginIntentResolverResult = { aLoginParams },
oidcIntentResolverResult = { null },
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
@@ -263,16 +242,15 @@ class IntentResolverTest {
private fun createIntentResolver(
permalinkParserResult: (String) -> PermalinkData = { lambdaError() },
loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() },
oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() },
): IntentResolver {
return IntentResolver(
deeplinkParser = DeeplinkParser(),
loginIntentResolver = FakeLoginIntentResolver(
parseResult = loginIntentResolverResult,
),
oidcIntentResolver = DefaultOidcIntentResolver(
oidcUrlParser = DefaultOidcUrlParser(
oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(),
)
oidcIntentResolver = FakeOidcIntentResolver(
resolveResult = oidcIntentResolverResult,
),
permalinkParser = FakePermalinkParser(
result = permalinkParserResult

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
<string name="screen_analytics_settings_read_terms">"Weitere Informationen findest du %1$s."</string>
<string name="screen_analytics_settings_read_terms">"Sie können unsere Bedingungen %1$s lesen."</string>
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
<string name="screen_analytics_settings_share_data">"Analysedaten teilen"</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"مسائل کی نشاندہی کرنے میں ہماری مدد کے لیے گمنام استعمال کے بیانات کا اشتراک کریں۔"</string>
<string name="screen_analytics_settings_read_terms">"آپ ہماری تمام شرائط پڑھ سکتے ہیں %1$s۔"</string>
<string name="screen_analytics_settings_read_terms_content_link">"یہاں"</string>
<string name="screen_analytics_settings_share_data">"تجزیاتی بیانات کا اشتراک کریں"</string>
</resources>

View File

@@ -2,9 +2,9 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string>
<string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
<string name="screen_analytics_prompt_read_terms">"Weitere Informationen findest du %1$s."</string>
<string name="screen_analytics_prompt_read_terms">"Sie können unsere Bedingungen %1$s lesen."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
<string name="screen_analytics_prompt_settings">"Du kannst diese Funktion jederzeit deaktivieren"</string>
<string name="screen_analytics_prompt_settings">"Sie können dies jederzeit beenden"</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>

View File

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

View File

@@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
@@ -62,6 +63,7 @@ class CallScreenPresenter @AssistedInject constructor(
private val activeCallManager: ActiveCallManager,
private val languageTagProvider: LanguageTagProvider,
private val appForegroundStateService: AppForegroundStateService,
private val activeRoomsHolder: ActiveRoomsHolder,
private val appCoroutineScope: CoroutineScope,
) : Presenter<CallScreenState> {
@AssistedFactory
@@ -241,8 +243,10 @@ class CallScreenPresenter @AssistedInject constructor(
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
if (!notifiedCallStart) {
getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
?.onSuccess { notifiedCallStart = true }
val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
val sendCallNotificationResult = activeRoomForSession?.sendCallNotificationIfNeeded()
?: getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
sendCallNotificationResult?.onSuccess { notifiedCallStart = true }
}
}

View File

@@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import kotlinx.coroutines.flow.firstOrNull
import javax.inject.Inject
@@ -24,6 +25,7 @@ class DefaultCallWidgetProvider @Inject constructor(
private val matrixClientsProvider: MatrixClientProvider,
private val appPreferencesStore: AppPreferencesStore,
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
private val activeRoomsHolder: ActiveRoomsHolder,
) : CallWidgetProvider {
override suspend fun getWidget(
sessionId: SessionId,
@@ -33,7 +35,9 @@ class DefaultCallWidgetProvider @Inject constructor(
theme: String?,
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
val room = matrixClient.getJoinedRoom(roomId) ?: error("Room not found")
val room = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
?: matrixClient.getJoinedRoom(roomId)
?: error("Room not found")
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL

View File

@@ -3,4 +3,5 @@
<string name="call_foreground_service_channel_title_android">"Chamada em andamento"</string>
<string name="call_foreground_service_message_android">"Toque para retornar à chamada"</string>
<string name="call_foreground_service_title_android">"☎️ Chamada em andamento"</string>
<string name="screen_incoming_call_subtitle_android">"Chamada do Element recebida"</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"جاری مکالمہ"</string>
<string name="call_foreground_service_message_android">"مکالمہ پر واپس جانے کے لیے تھپتھپائیں"</string>
<string name="call_foreground_service_title_android">"☎️ مکالمہ جاری ہے"</string>
<string name="screen_incoming_call_subtitle_android">"ورودی ایلیمنٹ کال"</string>
</resources>

View File

@@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.analytics.test.FakeScreenTracker
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.tests.testutils.WarmUpRule
@@ -367,6 +368,7 @@ import kotlin.time.Duration.Companion.seconds
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
screenTracker: ScreenTracker = FakeScreenTracker(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
): CallScreenPresenter {
val userAgentProvider = object : UserAgentProvider {
override fun provide(): String {
@@ -387,6 +389,7 @@ import kotlin.time.Duration.Companion.seconds
languageTagProvider = FakeLanguageTagProvider("en-US"),
appForegroundStateService = appForegroundStateService,
appCoroutineScope = backgroundScope,
activeRoomsHolder = activeRoomsHolder,
)
}
}

View File

@@ -40,11 +40,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
@@ -63,7 +59,6 @@ class DefaultActiveCallManagerTest {
fun `registerIncomingCall - sets the incoming call as active`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
inCancellableScope {
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
assertThat(manager.activeWakeLock?.isHeld).isFalse()
@@ -87,14 +82,12 @@ class DefaultActiveCallManagerTest {
assertThat(manager.activeWakeLock?.isHeld).isTrue()
verify { notificationManagerCompat.notify(notificationId, any()) }
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest {
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
val onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
inCancellableScope {
val manager = createActiveCallManager(
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
)
@@ -116,12 +109,10 @@ class DefaultActiveCallManagerTest {
.isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID))
}
}
@Test
fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest {
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
inCancellableScope {
val manager = createActiveCallManager(
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
)
@@ -130,7 +121,6 @@ class DefaultActiveCallManagerTest {
addMissedCallNotificationLambda.assertions().isNeverCalled()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
@@ -138,7 +128,6 @@ class DefaultActiveCallManagerTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
inCancellableScope {
val manager = createActiveCallManager(
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda),
notificationManagerCompat = notificationManagerCompat,
@@ -156,14 +145,11 @@ class DefaultActiveCallManagerTest {
addMissedCallNotificationLambda.assertions().isCalledOnce()
verify { notificationManagerCompat.cancel(notificationId) }
}
}
@Test
fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
// Create a cancellable coroutine scope to cancel the test when needed
inCancellableScope {
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
val notificationData = aCallNotificationData()
@@ -177,14 +163,11 @@ class DefaultActiveCallManagerTest {
verify { notificationManagerCompat.cancel(notificationId) }
}
}
@Test
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
// Create a cancellable coroutine scope to cancel the test when needed
inCancellableScope {
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
manager.registerIncomingCall(aCallNotificationData())
@@ -197,13 +180,11 @@ class DefaultActiveCallManagerTest {
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest {
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
inCancellableScope {
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
assertThat(manager.activeCall.value).isNull()
@@ -222,7 +203,6 @@ class DefaultActiveCallManagerTest {
verify { notificationManagerCompat.cancel(notificationId) }
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
@@ -233,8 +213,6 @@ class DefaultActiveCallManagerTest {
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
// Create a cancellable coroutine scope to cancel the test when needed
inCancellableScope {
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) })
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
@@ -249,7 +227,6 @@ class DefaultActiveCallManagerTest {
assertThat(manager.activeCall.value).isNull()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
@@ -260,8 +237,6 @@ class DefaultActiveCallManagerTest {
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
// Create a cancellable coroutine scope to cancel the test when needed
inCancellableScope {
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("Matrix client not found")) })
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
@@ -291,14 +266,6 @@ class DefaultActiveCallManagerTest {
// The call should still be active
assertThat(manager.activeCall.value).isNotNull()
}
}
private fun TestScope.inCancellableScope(block: suspend CoroutineScope.() -> Unit) {
launch(SupervisorJob()) {
block()
cancel()
}
}
private fun setupShadowPowerManager() {
shadowOf(InstrumentationRegistry.getInstrumentation().targetContext.getSystemService<PowerManager>()).apply {
@@ -306,14 +273,13 @@ class DefaultActiveCallManagerTest {
}
}
private fun CoroutineScope.createActiveCallManager(
private fun TestScope.createActiveCallManager(
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
coroutineScope: CoroutineScope = this,
) = DefaultActiveCallManager(
context = InstrumentationRegistry.getInstrumentation().targetContext,
coroutineScope = coroutineScope,
coroutineScope = backgroundScope,
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
ringingCallNotificationCreator = RingingCallNotificationCreator(
context = InstrumentationRegistry.getInstrumentation().targetContext,

View File

@@ -15,11 +15,13 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
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.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -77,6 +79,29 @@ class DefaultCallWidgetProviderTest {
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull()
}
@Test
fun `getWidget - reuses the active room if possible`() = runTest {
val client = FakeMatrixClient().apply {
// No room from the client
givenGetRoomResult(A_ROOM_ID, null)
}
val activeRoomsHolder = ActiveRoomsHolder().apply {
// A current active room with the same room id
addRoom(
FakeJoinedRoom(
baseRoom = FakeBaseRoom(roomId = A_ROOM_ID),
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
)
)
}
val provider = createProvider(
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
activeRoomsHolder = activeRoomsHolder
)
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isSuccess).isTrue()
}
@Test
fun `getWidget - will use a custom base url if it exists`() = runTest {
val room = FakeJoinedRoom(
@@ -104,9 +129,11 @@ class DefaultCallWidgetProviderTest {
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
) = DefaultCallWidgetProvider(
matrixClientsProvider = matrixClientProvider,
appPreferencesStore = appPreferencesStore,
callWidgetSettingsProvider = callWidgetSettingsProvider,
activeRoomsHolder = activeRoomsHolder,
)
}

View File

@@ -1,17 +1,17 @@
<?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">"Neuer Raum"</string>
<string name="screen_create_room_add_people_title">"Personen einladen"</string>
<string name="screen_create_room_add_people_title">"Nutzer einladen"</string>
<string name="screen_create_room_error_creating_room">"Beim Erstellen des Chats ist ein Fehler aufgetreten"</string>
<string name="screen_create_room_private_option_description">"Nur eingeladene Personen haben Zutritt zu diesem Chatroom. Alle Nachrichten sind durchgehend verschlüsselt."</string>
<string name="screen_create_room_private_option_description">"Nur eingeladene Personen haben Zutritt zu diesem Chatroom. Alle Nachrichten sind Ende-zu-Ende verschlüsselt."</string>
<string name="screen_create_room_private_option_title">"Privater Chatroom"</string>
<string name="screen_create_room_public_option_description">"Jeder kann diesen Chatroom finden.
<string name="screen_create_room_public_option_description">"Alle können diesen Chatroom finden.
Sie können dies aber jederzeit in den Chatroomeinstellungen ändern."</string>
<string name="screen_create_room_public_option_title">"Öffentlicher Raum"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Jeder kann diesem Chatroom beitreten"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Jemand"</string>
<string name="screen_create_room_room_access_section_header">"Chatroom Zugang"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Jeder kann darum bitten, dem Chatroom beizutreten, aber ein Administrator oder ein Moderator muss die Anfrage akzeptieren."</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Jeder darf diesen Raum betreten"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Jeder"</string>
<string name="screen_create_room_room_access_section_header">"Chatroomzugang"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Jeder kann den Zutritt zum Raum beantragen, aber ein Moderator muss die Anfrage akzeptieren."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Beitritt beantragen"</string>
<string name="screen_create_room_room_address_section_footer">"Damit dieser Chatroom im öffentlichen Chatroomverzeichnis sichtbar ist, benötigen Sie eine Chatroomadresse."</string>
<string name="screen_create_room_room_address_section_title">"Chatroomadresse"</string>

View File

@@ -7,9 +7,24 @@
<string name="screen_create_room_private_option_title">"Sala privada"</string>
<string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala.
Você pode mudar isso a qualquer momento nas configurações da sala."</string>
<string name="screen_create_room_public_option_title">"Sala pública"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Qualquer pessoa pode entrar nesta sala"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Qualquer pessoa"</string>
<string name="screen_create_room_room_access_section_header">"Acesso à sala"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou moderador terá de aceitar a solicitação"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Pedir para entrar"</string>
<string name="screen_create_room_room_address_section_footer">"Para que esta sala fique visível no diretório público de salas, você precisará de um endereço de sala."</string>
<string name="screen_create_room_room_address_section_title">"Endereço da sala"</string>
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilidade da sala"</string>
<string name="screen_create_room_title">"Criar uma sala"</string>
<string name="screen_create_room_topic_label">"Tópico (opcional)"</string>
<string name="screen_room_directory_search_title">"Diretório de salas"</string>
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar um chat"</string>
<string name="screen_start_chat_join_room_by_address_action">"Entrar na sala pelo endereço"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Não é um endereço válido"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Entrar…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Foi encontrada uma sala correspondente"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Sala não encontrada"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"Por exemplo, #nome-da-sala:matrix.org"</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?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_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_title">"ایک کمرہ بنائیں"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
<string name="screen_room_directory_search_title">"کمرے کا راہنامچہ"</string>
<string name="screen_start_chat_error_starting_chat">"گفتگو شروع کرنے کی کوشش کرتے وقت ایک خرابی واقع ہوگئی"</string>
</resources>

View File

@@ -10,5 +10,5 @@
<string name="screen_deactivate_account_list_item_2">"Sie werden aus allen Chatrooms entfernt."</string>
<string name="screen_deactivate_account_list_item_3">"Löschen Sie Ihre Kontoinformationen von unserem Identitätsserver."</string>
<string name="screen_deactivate_account_list_item_4">"Gelöschte Nachrichten werden für bereits registrierte Benutzer weiterhin sichtbar sein, wenn sie auch neuen oder nicht registrierten Benutzern nicht mehr zur Verfügung stehen."</string>
<string name="screen_deactivate_account_title">"Benutzerkonto deaktivieren"</string>
<string name="screen_deactivate_account_title">"Nutzerkonto deaktivieren"</string>
</resources>

View File

@@ -1,6 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Confirme que você deseja desativar sua conta. Essa ação não pode ser desfeita."</string>
<string name="screen_deactivate_account_delete_all_messages">"Excluir todas as minhas mensagens"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Aviso: Os futuros usuários poderão ver conversas incompletas."</string>
<string name="screen_deactivate_account_description">"Desativar sua conta é %1$s, isso irá:"</string>
<string name="screen_deactivate_account_description_bold_part">"irreversível"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s sua conta (você não poderá fazer login novamente, e seu ID não poderá ser reutilizado)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Desativar permanentemente"</string>
<string name="screen_deactivate_account_list_item_2">"Te remover de todas as salas de conversa."</string>
<string name="screen_deactivate_account_list_item_3">"Exclua as informações da sua conta do nosso servidor de identidade."</string>
<string name="screen_deactivate_account_list_item_4">"Suas mensagens ainda estarão visíveis para os usuários registrados, mas não estarão disponíveis para usuários novos ou não registrados se você optar por excluí-las."</string>
<string name="screen_deactivate_account_title">"Desativar conta"</string>
</resources>

View File

@@ -16,9 +16,21 @@ interface EnterpriseService {
fun defaultHomeserverList(): List<String>
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
suspend fun isElementCallAvailable(): Boolean
fun semanticColorsLight(): SemanticColors
fun semanticColorsDark(): SemanticColors
fun firebasePushGateway(): String?
fun unifiedPushDefaultPushGateway(): String?
companion object {
const val ANY_ACCOUNT_PROVIDER = "*"
}
}
fun EnterpriseService.canConnectToAnyHomeserver(): Boolean {
return defaultHomeserverList().let {
it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER)
}
}

View File

@@ -25,6 +25,8 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService {
override fun defaultHomeserverList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
override suspend fun isElementCallAvailable(): Boolean = true
override fun semanticColorsLight(): SemanticColors = compoundColorsLight
override fun semanticColorsDark(): SemanticColors = compoundColorsDark

View File

@@ -18,6 +18,7 @@ class FakeEnterpriseService(
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
@@ -35,6 +36,10 @@ class FakeEnterpriseService(
isAllowedToConnectToHomeserverResult(homeserverUrl)
}
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
isElementCallAvailableResult()
}
override fun semanticColorsLight(): SemanticColors {
return semanticColorsLightResult()
}

View File

@@ -57,7 +57,6 @@ dependencies {
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.analytics.noop)
testImplementation(projects.libraries.permissions.impl)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.features.lockscreen.test)

View File

@@ -1,22 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Sie können es nicht bestätigen?"</string>
<string name="screen_identity_confirmation_cannot_confirm">"Bestätigung unmöglich?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Erstelle einen neuen Wiederherstellungsschlüssel"</string>
<string name="screen_identity_confirmation_subtitle">"Verifiziere dieses Gerät, um sicheres Messaging einzurichten."</string>
<string name="screen_identity_confirmation_title">"Bestätige, dass du es bist"</string>
<string name="screen_identity_confirmation_title">"Bestätigen Sie Ihre Identität"</string>
<string name="screen_identity_confirmation_use_another_device">"Ein anderes Gerät verwenden"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Wiederherstellungsschlüssel verwenden"</string>
<string name="screen_identity_confirmed_subtitle">"Du kannst nun verschlüsselte Nachrichten lesen oder versenden."</string>
<string name="screen_identity_confirmed_subtitle">"Sie können jetzt verschlüsselte Nachrichten lesen und versenden. Ihre Chatpartner vertrauen nun diesem Gerät auch."</string>
<string name="screen_identity_confirmed_title">"Gerät verifiziert"</string>
<string name="screen_identity_use_another_device">"Ein anderes Gerät verwenden"</string>
<string name="screen_identity_waiting_on_other_device">"Bitte warten bis das andere Gerät bereit ist."</string>
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string>
<string name="screen_notification_optin_subtitle">"Sie können Ihre Einstellungen später ändern."</string>
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
<string name="screen_session_verification_enter_recovery_key">"Wiederherstellungsschlüssel eingeben"</string>
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres 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, von dir zu hören. Teile uns deine Meinung über die Einstellungsseite mit."</string>
<string name="screen_welcome_button">"Los geht\'s!"</string>
<string name="screen_welcome_subtitle">"Folgendes musst du wissen:"</string>
<string name="screen_welcome_subtitle">"Folgendes sollten Sie wissen:"</string>
<string name="screen_welcome_title">"Willkommen bei %1$s!"</string>
</resources>

View File

@@ -1,8 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Não consegue confirmar?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Criar uma nova chave de recuperação"</string>
<string name="screen_identity_confirmation_subtitle">"Verifique este dispositivo para configurar mensagens seguras."</string>
<string name="screen_identity_confirmation_title">"Confirme sua identidade"</string>
<string name="screen_identity_confirmation_use_another_device">"Usar outro dispositivo"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Use a chave de recuperação"</string>
<string name="screen_identity_confirmed_subtitle">"Agora você pode ler ou enviar mensagens com segurança, e qualquer pessoa com quem você conversa também pode confiar neste dispositivo."</string>
<string name="screen_identity_confirmed_title">"Dispositivo verificado"</string>
<string name="screen_identity_use_another_device">"Usar outro dispositivo"</string>
<string name="screen_identity_waiting_on_other_device">"Aguardando outro dispositivo…"</string>
<string name="screen_notification_optin_subtitle">"Você pode alterar suas configurações mais tarde."</string>
<string name="screen_notification_optin_title">"Permita notificações e nunca perca uma mensagem"</string>
<string name="screen_session_verification_enter_recovery_key">"Insira a chave de recuperação"</string>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"تصدیق نہیں کر سکتے؟"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"ایک نئی بازیابی کلید تخلیق کریں"</string>
<string name="screen_identity_confirmation_subtitle">"محفوظ پیغام رسانی ترتیب دینے کیلئے اس آلے کی توثیق کریں۔"</string>
<string name="screen_identity_confirmation_title">"اپنی شناخت کی تصدیق کریں"</string>
<string name="screen_identity_confirmation_use_another_device">"دوسرا آلہ استعمال کریں"</string>
<string name="screen_identity_confirmation_use_recovery_key">"بازیابی کلید استعمال کریں"</string>
<string name="screen_identity_confirmed_subtitle">"اب آپ محفوظ طریقے سے پیغامات پڑھ یا بھیج سکتے ہیں، اور جسکے ساتھ آپ گفتگو کرتے ہیں وہ بھی اس آلہ پر بھروسہ کر سکتا ہے۔"</string>
<string name="screen_identity_confirmed_title">"آلہ توثیق شدہ"</string>
<string name="screen_identity_use_another_device">"دوسرا آلہ استعمال کریں"</string>
<string name="screen_identity_waiting_on_other_device">"دوسرے آلہ پر منتظر…"</string>
<string name="screen_notification_optin_subtitle">"آپ بعد میں اپنی ترتیبات تبدیل کر سکتے ہیں۔"</string>
<string name="screen_notification_optin_title">"اطلاعات کی اجازت دیں اور کبھی بھی کسی پیغام سے محروم نہ ہوں۔"</string>
<string name="screen_session_verification_enter_recovery_key">"بازیابی کلید درج کریں"</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>

View File

@@ -19,7 +19,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
import io.element.android.libraries.permissions.test.FakePermissionStateProvider
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.services.analytics.api.AnalyticsService

View File

@@ -14,7 +14,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
import io.element.android.libraries.permissions.test.FakePermissionStateProvider
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider

View File

@@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Sie werden keine Nachrichten oder Chatroomeinladungen von diesem Benutzer sehen."</string>
<string name="screen_decline_and_block_block_user_option_description">"Sie werden keine Nachrichten oder Chateinladungen von diesem Nutzer sehen."</string>
<string name="screen_decline_and_block_block_user_option_title">"Benutzer blockieren"</string>
<string name="screen_decline_and_block_report_user_option_description">"Melden Sie diesen Raum Ihrem Kontoanbieter."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Beschreiben Sie den Grund für die Meldung…"</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Nennen Sie den Grund für die Meldung…"</string>
<string name="screen_decline_and_block_title">"Ablehnen und blockieren"</string>
<string name="screen_invites_decline_chat_message">"Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?"</string>
<string name="screen_invites_decline_chat_message">"Möchten Sie die Einladung zum Betreten von %1$s wirklich ablehnen?"</string>
<string name="screen_invites_decline_chat_title">"Einladung ablehnen"</string>
<string name="screen_invites_decline_direct_chat_message">"Bist du sicher, dass du diese Direktnachricht von %1$s ablehnen möchtest?"</string>
<string name="screen_invites_decline_direct_chat_message">"Möchten Sie diesen privaten Chat mit %1$s wirklich ablehnen?"</string>
<string name="screen_invites_decline_direct_chat_title">"Einladung ablehnen"</string>
<string name="screen_invites_empty_list">"Keine Einladungen"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) hat dich eingeladen"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ja, ablehnen und blockieren"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ja, ablehnen &amp; blockieren"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Sind Sie sicher, dass Sie die Einladung zu diesem Raum ablehnen möchten? Dadurch wird auch verhindert, dass %1$s Sie kontaktiert oder in Räume einlädt."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Einladung ablehnen und blockieren"</string>
<string name="screen_join_room_decline_and_block_alert_title">"Einladung ablehnen &amp; Nutzer blockieren"</string>
<string name="screen_join_room_decline_and_block_button_title">"Ablehnen und blockieren"</string>
</resources>

View File

@@ -1,9 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Você não verá nenhuma mensagem ou convite de sala deste usuário"</string>
<string name="screen_decline_and_block_block_user_option_title">"Bloquear usuário"</string>
<string name="screen_decline_and_block_report_user_option_description">"Denuncie esta sala ao fornecedor da sua conta."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Descreva o motivo da denúncia…"</string>
<string name="screen_decline_and_block_title">"Recusar e bloquear"</string>
<string name="screen_invites_decline_chat_message">"Tem certeza de que deseja recusar o convite para ingressar em %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Recusar convite"</string>
<string name="screen_invites_decline_direct_chat_message">"Tem certeza de que deseja recusar esse chat privado com %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Recusar chat"</string>
<string name="screen_invites_empty_list">"Sem convites"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s) convidou você"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Sim, recusar e bloquear"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Você tem certeza de que deseja recusar o convite para participar desta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para as salas."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Recusar convite e bloquear"</string>
<string name="screen_join_room_decline_and_block_button_title">"Recusar e bloquear"</string>
</resources>

View File

@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Não verás quaisquer mensagens ou convites deste utilizador"</string>
<string name="screen_decline_and_block_block_user_option_title">"Bloquear utilizador"</string>
<string name="screen_decline_and_block_report_user_option_description">"Denunciar esta sala ao teu operador de conta."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Descreve a razão de denúncia…"</string>
<string name="screen_decline_and_block_title">"Rejeitar e bloquear"</string>
<string name="screen_invites_decline_chat_message">"Tens a certeza que queres rejeitar o convite para entra em %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Rejeitar convite"</string>
<string name="screen_invites_decline_direct_chat_message">"Tem a certeza que queres rejeitar esta conversa privada com %1$s?"</string>

View File

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

View File

@@ -3,6 +3,7 @@
<string name="screen_decline_and_block_block_user_option_description">"您將不會看到來自此使用者的任何訊息或聊天室邀請"</string>
<string name="screen_decline_and_block_block_user_option_title">"封鎖使用者"</string>
<string name="screen_decline_and_block_report_user_option_description">"向您的帳號提供者回報此聊天室。"</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"說明回報的原因……"</string>
<string name="screen_decline_and_block_title">"拒絕並封鎖"</string>
<string name="screen_invites_decline_chat_message">"您確定您想要拒絕加入 %1$s 的邀請嗎?"</string>
<string name="screen_invites_decline_chat_title">"拒絕邀請"</string>

View File

@@ -1,32 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"Sie wurden aus diesem Chatroom ausgeschlossen %1$s."</string>
<string name="screen_join_room_ban_message">"Sie wurden aus diesem Chatroom verbannt"</string>
<string name="screen_join_room_ban_by_message">"Sie wurden von %1$s für diesen Chatroom gesperrt."</string>
<string name="screen_join_room_ban_message">"Sie wurden für diesen Chatroom gesperrt"</string>
<string name="screen_join_room_ban_reason">"Grund:%1$s."</string>
<string name="screen_join_room_cancel_knock_action">"Anfrage abbrechen"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ja, abbrechen"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Möchten Sie Ihre Beitrittsanfrage für diesen Chatroom wirklich stornieren?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Beitrittsanfrage stornieren"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ja, ablehnen und blockieren"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ja, ablehnen &amp; blockieren"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Sind Sie sicher, dass Sie die Einladung zu diesem Raum ablehnen möchten? Dadurch wird auch verhindert, dass %1$s Sie kontaktiert oder in Räume einlädt."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Einladung ablehnen und blockieren"</string>
<string name="screen_join_room_decline_and_block_alert_title">"Einladung ablehnen &amp; Nutzer blockieren"</string>
<string name="screen_join_room_decline_and_block_button_title">"Ablehnen und blockieren"</string>
<string name="screen_join_room_fail_message">"Der Beitritt zum Chatroom schlug fehl."</string>
<string name="screen_join_room_fail_reason">"Dieser Chatroom ist entweder nur auf Einladung zugänglich oder es kann zu Zugangsbeschränkungen auf Spaceebene kommen."</string>
<string name="screen_join_room_fail_reason">"Dieser Chatroom ist entweder nur auf Einladung zugänglich oder es gibt andere Zugangsbeschränkungen durch Spaces."</string>
<string name="screen_join_room_forget_action">"Vergessen Sie diesen Raum"</string>
<string name="screen_join_room_invite_required_message">"Sie benötigen eine Einladung, um diesem Raum beizutreten"</string>
<string name="screen_join_room_invite_required_message">"Sie benötigen eine Einladung, um diesem Chatroom zu betreten"</string>
<string name="screen_join_room_join_action">"Raum beitreten"</string>
<string name="screen_join_room_join_restricted_message">"Möglicherweise müssen Sie eingeladen sein oder Mitglied eines Spaces sein, um beitreten zu können."</string>
<string name="screen_join_room_knock_action">"Anklopfen"</string>
<string name="screen_join_room_knock_message_description">"Nachricht (optional)"</string>
<string name="screen_join_room_knock_sent_description">"Falls Ihre Anfrage, dem Raum beizutreten, akzeptiert wird, werden Sie eine Einladung erhalten."</string>
<string name="screen_join_room_knock_sent_description">"Falls Ihre Anfrage, den Raum zu betreten, akzeptiert wird, erhalten Sie eine Einladung."</string>
<string name="screen_join_room_knock_sent_title">"Beitrittsanfrage geschickt"</string>
<string name="screen_join_room_loading_alert_message">"Wir konnten die Chatroomvorschau nicht anzeigen. Dies kann an Netzwerk- oder Serverproblemen liegen."</string>
<string name="screen_join_room_loading_alert_title">"Wir konnten diese Chatroomvorschau nicht anzeigen"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s unterstützt noch keine Spaces. Du kannst auf Spaces im Web zugreifen."</string>
<string name="screen_join_room_space_not_supported_description">"%1$s unterstützt noch keine Spaces. Sie können auf Spaces im Web zugreifen."</string>
<string name="screen_join_room_space_not_supported_title">"Spaces werden noch nicht unterstützt"</string>
<string name="screen_join_room_subtitle_knock">"Klopfe an um einen Administrator zu benachrichtigen. Nach der Freigabe kannst du dich an der Unterhaltung beteiligen."</string>
<string name="screen_join_room_subtitle_no_preview">"Du musst Mitglied in diesem Raum sein, um den Nachrichtenverlauf zu sehen."</string>
<string name="screen_join_room_title_knock">"Willst du diesem Raum beitreten?"</string>
<string name="screen_join_room_subtitle_knock">"Klicken Sie auf die Schaltfläche unten und ein Chatroomadministrator wird benachrichtigt. Nach der Freigabe durch einen Chatroomadministrator können Sie sich an der Unterhaltung beteiligen."</string>
<string name="screen_join_room_subtitle_no_preview">"Sie müssen Mitglied in diesem Chatroom sein, um den Nachrichtenverlauf einsehen zu können."</string>
<string name="screen_join_room_title_knock">"Möchten Sie diesem Chatroom betreten?"</string>
<string name="screen_join_room_title_no_preview">"Vorschau nicht verfügbar"</string>
</resources>

View File

@@ -3,6 +3,7 @@
<string name="screen_join_room_ban_reason">"Arrazoia: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"Utzi eskaera bertan behera"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Bai, utzi bertan behera"</string>
<string name="screen_join_room_fail_message">"Gelara sartzeak huts egin du."</string>
<string name="screen_join_room_forget_action">"Ahaztu gela hau"</string>
<string name="screen_join_room_join_action">"Sartu gelan"</string>
<string name="screen_join_room_knock_action">"Bidali batzeko eskaera"</string>

View File

@@ -1,4 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"Você foi banido desta sala por %1$s."</string>
<string name="screen_join_room_ban_message">"Você foi banido desta sala"</string>
<string name="screen_join_room_ban_reason">"Motivo: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"Cancelar pedido"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Sim, cancele"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Tem a certeza de que pretende cancelar o seu pedido de adesão a esta sala?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Cancelar pedido de adesão"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Sim, recusar e bloquear"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Você tem certeza de que deseja recusar o convite para participar desta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para as salas."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Recusar convite e bloquear"</string>
<string name="screen_join_room_decline_and_block_button_title">"Recusar e bloquear"</string>
<string name="screen_join_room_fail_message">"A entrada na sala falhou."</string>
<string name="screen_join_room_fail_reason">"Esta sala é apenas para convidados ou pode haver restrições de acesso no nível do espaço."</string>
<string name="screen_join_room_forget_action">"Esquecer esta sala"</string>
<string name="screen_join_room_invite_required_message">"Você precisa de um convite para entrar nesta sala"</string>
<string name="screen_join_room_join_action">"Entrar na sala"</string>
<string name="screen_join_room_join_restricted_message">"Talvez você precise ser convidado ou ser membro de um espaço para participar."</string>
<string name="screen_join_room_knock_action">"Enviar solicitação para participar"</string>
<string name="screen_join_room_knock_message_description">"Mensagem (opcional)"</string>
<string name="screen_join_room_knock_sent_description">"Você receberá um convite para participar da sala se seu pedido for aceito."</string>
<string name="screen_join_room_knock_sent_title">"Pedido de adesão enviado"</string>
<string name="screen_join_room_loading_alert_message">"Não foi possível exibir a visualização da sala. Isso pode ser devido a problemas de rede ou de servidor."</string>
<string name="screen_join_room_loading_alert_title">"Não foi possível exibir a visualização desta sala"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s não suporta espaços ainda. Você pode acessar os espaços na web."</string>
<string name="screen_join_room_space_not_supported_title">"Os espaços ainda não são compatíveis"</string>
<string name="screen_join_room_subtitle_knock">"Clique no botão abaixo e um administrador da sala será notificado. Você poderá participar da conversa assim que for aprovado."</string>
<string name="screen_join_room_subtitle_no_preview">"Você deve ser um membro desta sala para visualizar o histórico de mensagens."</string>
<string name="screen_join_room_title_knock">"Quer entrar nesta sala?"</string>
<string name="screen_join_room_title_no_preview">"A pré-visualização não está disponível"</string>
</resources>

View 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_join_room_join_action">"کمرے میں شامل ہوں"</string>
<string name="screen_join_room_knock_action">"شامل ہونے کی درخواست بھیجیں"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s ابھی تک خالی جگہوں کی حمایت نہیں کرتا۔ آپ جال پر خالی جگہوں تک رسائی حاصل کرسکتے ہیں۔"</string>
<string name="screen_join_room_space_not_supported_title">"ابھی تک جگہیں تعاون یافتہ نہیں"</string>
<string name="screen_join_room_subtitle_knock">"نیچے دیئے گئے کلید پر دبائیں اور کمرے کے منتظم کو مطلع کیا جائے گا۔ منظور ہونے کے بعد آپ گفتگو میں شامل ہو سکیں گے۔"</string>
<string name="screen_join_room_subtitle_no_preview">"پیغام کی سرگزشت دیکھنے کے لیے آپ کا اس کمرے کا رکن ہونا ضروری ہے۔"</string>
<string name="screen_join_room_title_knock">"اس کمرے میں شامل ہونا چاہتے ہیں؟"</string>
<string name="screen_join_room_title_no_preview">"پیش منظر دستیاب نہیں ہے"</string>
</resources>

View File

@@ -25,6 +25,14 @@
<string name="screen_knock_requests_list_empty_state_title">"Dim cais i ymuno yn disgwyl"</string>
<string name="screen_knock_requests_list_initial_loading_title">"Yn llwytho ceisiadau i ymuno…"</string>
<string name="screen_knock_requests_list_title">"Ceisiadau i ymuno"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="zero">"Dyw %1$s na +%2$d arall eisiau ymuno â\'r ystafell hon"</item>
<item quantity="one">"Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon"</item>
<item quantity="two">"Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon"</item>
<item quantity="few">"Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon"</item>
<item quantity="many">"Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon"</item>
<item quantity="other">"Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Gweld y cyfan"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Derbyn"</string>
<string name="screen_room_single_knock_request_title">"Mae %1$s eisiau ymuno â\'r ystafell hon"</string>

View File

@@ -11,7 +11,7 @@
<string name="screen_knock_requests_list_accept_failed_alert_title">"Die Anfrage konnte nicht akzeptiert werden"</string>
<string name="screen_knock_requests_list_accept_loading_title">"Beitrittsanfrage annehmen"</string>
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Ja, ablehnen und sperren"</string>
<string name="screen_knock_requests_list_ban_alert_description">"Sind Sie sicher, dass Sie %1$s ablehnen und sperren möchten ? Dieser Benutzer kann keinen erneuten Zugriff auf diesen Raum anfordern."</string>
<string name="screen_knock_requests_list_ban_alert_description">"Sind Sie sicher, dass Sie %1$s ablehnen und sperren möchten?Dieser Benutzer kann keine erneute Zulassung auf diesen Chatroom anfordern."</string>
<string name="screen_knock_requests_list_ban_alert_title">"Ablehnen und Zugriff verbieten"</string>
<string name="screen_knock_requests_list_ban_loading_title">"Ablehnung und Sperrung des Zugriffs"</string>
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Ja, ablehnen"</string>

View File

@@ -1,4 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_knock_requests_list_accept_all_alert_confirm_button_title">"Sim, aceitar todos"</string>
<string name="screen_knock_requests_list_accept_all_alert_description">"Tem certeza de que deseja aceitar todos os pedidos de adesão?"</string>
<string name="screen_knock_requests_list_accept_all_alert_title">"Aceitar todos os pedidos"</string>
<string name="screen_knock_requests_list_accept_all_button_title">"Aceitar todos"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"Não pudemos aceitar todas as solicitações. Você gostaria de tentar novamente?"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Falha ao aceitar todas as solicitações"</string>
<string name="screen_knock_requests_list_accept_all_loading_title">"Aceitando todas as solicitações de adesão"</string>
<string name="screen_knock_requests_list_accept_failed_alert_description">"Não pudemos aceitar essa solicitação. Você gostaria de tentar novamente?"</string>
<string name="screen_knock_requests_list_accept_failed_alert_title">"Falha ao aceitar a solicitação"</string>
<string name="screen_knock_requests_list_accept_loading_title">"Aceitando solicitação de adesão"</string>
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Sim, recusar e banir"</string>
<string name="screen_knock_requests_list_ban_alert_description">"Você tem certeza de que deseja recusar e banir %1$s? Este usuário não poderá solicitar acesso para entrar nesta sala novamente."</string>
<string name="screen_knock_requests_list_ban_alert_title">"Recusar e proibir o acesso"</string>
<string name="screen_knock_requests_list_ban_loading_title">"Recusando e proibindo o acesso"</string>
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Sim, recusar"</string>
<string name="screen_knock_requests_list_decline_alert_description">"Você tem certeza de que deseja recusar a solicitação de %1$s para participar desta sala?"</string>
<string name="screen_knock_requests_list_decline_alert_title">"Recusar acesso"</string>
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Recusar e banir"</string>
<string name="screen_knock_requests_list_decline_failed_alert_description">"Não foi possível recusar esta solicitação. Você gostaria de tentar novamente?"</string>
<string name="screen_knock_requests_list_decline_failed_alert_title">"Falha ao recusar a solicitação"</string>
<string name="screen_knock_requests_list_decline_loading_title">"Recusando a solicitação de adesão"</string>
<string name="screen_knock_requests_list_empty_state_description">"Quando alguém pedir para entrar na sala, você poderá ver o pedido aqui."</string>
<string name="screen_knock_requests_list_empty_state_title">"Nenhum pedido pendente de adesão"</string>
<string name="screen_knock_requests_list_initial_loading_title">"Carregando solicitações para participar…"</string>
<string name="screen_knock_requests_list_title">"Solicitações para entrar"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s +%2$d outro desejam entrar desta sala"</item>
<item quantity="other">"%1$s +%2$d outros desejam entrar desta sala"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Ver tudo"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Aceitar"</string>
<string name="screen_room_single_knock_request_title">"%1$s quer entrar nesta sala"</string>
<string name="screen_room_single_knock_request_view_button_title">"Ver"</string>
</resources>

View File

@@ -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_room_single_knock_request_accept_button_title">"قبول کریں"</string>
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Bist du sicher, dass du diese Unterhaltung verlassen willst? Diese Unterhaltung ist nicht öffentlich und du kannst ihr ohne Einladung nicht wieder beitreten."</string>
<string name="leave_room_alert_empty_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Du bist die einzige Person hier. Wenn du gehst, kann in Zukunft niemand mehr eintreten, auch du nicht."</string>
<string name="leave_room_alert_private_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne Einladung nicht erneut beitreten."</string>
<string name="leave_room_alert_subtitle">"Bist du sicher, dass du den Raum verlassen willst?"</string>
<string name="leave_conversation_alert_subtitle">"Sind Sie sicher, dass Sie diesen Chat verlassen wollen? Dieser Chat ist nicht öffentlich und Sie können ihn ohne Einladung nicht wieder betreten."</string>
<string name="leave_room_alert_empty_subtitle">"Sind Sie sicher dass Sie diesen Chatroom verlassen möchten? Sie sind die einzige Person hier. Wenn Sie gehen, kann in Zukunft niemand mehr - auch Sie nicht - diesen Chatrooom betreten.."</string>
<string name="leave_room_alert_private_subtitle">"Sind Sie sicher dass Sie diesen Chatroom verlassen möchten? Dieser Chatroom ist nicht öffentlich und Sie können ihn ohne Einladung nicht wieder betreten."</string>
<string name="leave_room_alert_subtitle">"Sind Sie sicher, dass Sie den Raum verlassen möchten?"</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"کیا آپ واقعی اس گفتگو کو چھوڑنا چاہتے ہیں؟ یہ گفتگو عوامی نہیں ہے اور آپ دعوت نامے کے بغیر دوبارہ شامل نہیں ہو سکیں گے۔"</string>
<string name="leave_room_alert_empty_subtitle">"کیا آپ واقعی یہ کمرہ چھوڑنا چاہتے ہیں؟ آپ یہاں واحد شخص ہیں۔ اگر آپ چھوڑتے ہیں، تو مستقبل میں کوئی بھی شامل نہیں ہو سکے گا، آپ سمیت۔"</string>
<string name="leave_room_alert_private_subtitle">"کیا آپ واقعی یہ کمرہ چھوڑنا چاہتے ہیں؟ یہ کمرہ عوامی نہیں اور آپ دعوت نامے کے بغیر پھر شامل نہیں ہو پائیں گے۔"</string>
<string name="leave_room_alert_subtitle">"کیا آپ واقعی کمرہ چھوڑنا چاہتے ہیں؟"</string>
</resources>

View File

@@ -24,6 +24,22 @@ Dewiswch rywbeth cofiadwy. Os byddwch chi\'n anghofio\'r PIN hwn, byddwch chi\'n
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Nid yw\'r PINau\'n cyfateb"</string>
<string name="screen_app_lock_signout_alert_message">"Bydd angen i chi ail-fewngofnodi a chreu PIN newydd i barhau"</string>
<string name="screen_app_lock_signout_alert_title">"Rydych chi\'n cael eich allgofnodi"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="zero">"Does gennych %1$d ceisiadau i ddatgloi"</item>
<item quantity="one">"Mae gennych %1$d cais i ddatgloi"</item>
<item quantity="two">"Mae gennych %1$d gais i ddatgloi"</item>
<item quantity="few">"Mae gennych %1$d chais i ddatgloi"</item>
<item quantity="many">"Mae gennych %1$d chais i ddatgloi"</item>
<item quantity="other">"Mae gennych %1$d cais i ddatgloi"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="zero">"PIN anghywir. Does gennych %1$d cais arall"</item>
<item quantity="one">"PIN anghywir. Mae gennych %1$d cais arall"</item>
<item quantity="two">"PIN anghywir. Mae gennych %1$d gais arall"</item>
<item quantity="few">"PIN anghywir. Mae gennych %1$d chais arall"</item>
<item quantity="many">"PIN anghywir. Mae gennych %1$d chais arall"</item>
<item quantity="other">"PIN anghywir. Mae gennych %1$d cais arall"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Defnyddio biometreg"</string>
<string name="screen_app_lock_use_pin_android">"Defnyddio PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Yn allgofnodi…"</string>

View File

@@ -8,29 +8,29 @@
<string name="screen_app_lock_settings_change_pin">"PIN-Code ändern"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Biometrisches Entsperren zulassen"</string>
<string name="screen_app_lock_settings_remove_pin">"Pin entfernen"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Bist du sicher, dass du die PIN entfernen willst?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Sind Sie sicher, dass Sie die PIN entfernen wollen?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN entfernen?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s zulassen"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Ich möchte diese PIN verwenden."</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Spare dir etwas Zeit und benutze %1$s, um die App zu entsperren"</string>
<string name="screen_app_lock_setup_choose_pin">"PIN wählen"</string>
<string name="screen_app_lock_setup_confirm_pin">"PIN bestätigen"</string>
<string name="screen_app_lock_setup_pin_context">"Sperre %1$s mit einem PIN Code, um den Zugriff auf deine Chats zu beschränken.
<string name="screen_app_lock_setup_pin_context">"Erhöhen Sie die Sicherheit von %1$s mit einem PIN Code.
Wähle etwas Einprägsames. Bei falscher Eingabe wirst du aus der App ausgeloggt."</string>
Wählen Sie etwas Einprägsames. Wenn Sie die PIN vergessen, werden Sie aus der App ausgeloggt."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Bitte eine andere PIN verwenden."</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Bitte gib die gleiche PIN wie zuvor ein."</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Die PINs stimmen nicht überein"</string>
<string name="screen_app_lock_signout_alert_message">"Um fortzufahren, musst du dich erneut anmelden und eine neue PIN erstellen"</string>
<string name="screen_app_lock_signout_alert_title">"Du wirst abgemeldet"</string>
<string name="screen_app_lock_signout_alert_message">"Um fortzufahren, müssen Sie sich erneut anmelden und eine neue PIN erstellen"</string>
<string name="screen_app_lock_signout_alert_title">"Sie werden abgemeldet"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Du hast %1$d Versuch zu entsperren"</item>
<item quantity="other">"Du hast %1$d Versuche zum Entsperren"</item>
<item quantity="one">"Sie haben %1$d Entsperrversuch"</item>
<item quantity="other">"Sie haben %1$d Entsperrversuche"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Falsche PIN. Du hast %1$d weiteren Versuch"</item>
<item quantity="other">"Falsche PIN. Du hast %1$d weitere Versuche"</item>
<item quantity="one">"Falsche PIN. Sie haben %1$d weiteren Versuch"</item>
<item quantity="other">"Falsche PIN. Sie haben %1$d weitere Versuche"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Biometrie verwenden"</string>
<string name="screen_app_lock_use_pin_android">"PIN verwenden"</string>

View File

@@ -3,6 +3,7 @@
<string name="screen_app_lock_biometric_authentication">"autenticação por biometria"</string>
<string name="screen_app_lock_biometric_unlock">"desbloqueio por biometria"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Desbloquear com biometria"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"Confirmar biometria"</string>
<string name="screen_app_lock_forgot_pin">"Esqueceu o PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Alterar código de PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Permitir desbloqueio biométrico"</string>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"زیست سنجی تصدیق"</string>
<string name="screen_app_lock_biometric_unlock">"زیست سنجی فتحِ قفل"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"زیست سنجی کے ساتھ فتح قفل کریں"</string>
<string name="screen_app_lock_forgot_pin">"PIN بھول گئے؟"</string>
<string name="screen_app_lock_settings_change_pin">"PIN رمز بدلیں"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"زیست سنجی فتحِ قفل کی اجازت دیں"</string>
<string name="screen_app_lock_settings_remove_pin">"PIN ہٹائیں"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"کیا آپ کو یقین ہے کہ آپ PIN ہٹانا چاہتے ہیں؟"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN ہٹائیں؟"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s کی اجازت دیں"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"میں اس کے بجائے PIN استعمال کروں گا"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"اپنے آپ کو کچھ وقت بچائیں اور ہر بار اطلاقیے کو غیر مقفل کرنے کے لئے %1$s کا استعمال کریں۔"</string>
<string name="screen_app_lock_setup_choose_pin">"PIN چنیں"</string>
<string name="screen_app_lock_setup_confirm_pin">"PIN کی تصدیق کریں"</string>
<string name="screen_app_lock_setup_pin_context">"اپنی گفتگوہا میں اضافی سلامتی شامل کرنے کیلئے %1$s مقفل کریں۔
کوئی یادگار چیز چنیں۔ اگر آپ اس PIN کو بھول گئے، آپ طلاقیے سے خارج ہوجائیں گے۔"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"حفاظتی وجوہات کی بنا پر آپ اسے اپنے PIN رمز کے طور پر منتخب نہیں کر سکتے"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"ایک مختلف PIN چنیں"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"برائے مہربانی ایک ہی PIN دو بار درج کریں۔"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs مماثل نہیں ہیں"</string>
<string name="screen_app_lock_signout_alert_message">"آگے بڑھنے کیلئے آپکو دوبارہ داخل ہونے اور ایک نیا PIN بنانے کی ضرورت ہوگی۔"</string>
<string name="screen_app_lock_signout_alert_title">"آپکو خارج کیا جا رہا ہے"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"آپکے پاس %1$d غیر مقفل کرنے کی کوشش ہے"</item>
<item quantity="other">"آپکے پاس %1$d غیر مقفل کرنے کی کوششیں ہیں"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"غلط PIN۔ آپ کے پاس %1$d مزید موقع ہے"</item>
<item quantity="other">"غلط PIN۔ آپ کے پاس %1$d مزید موقعے ہیں"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"زیست سنجی استعمال کریں"</string>
<string name="screen_app_lock_use_pin_android">"PIN استعمال کریں"</string>
<string name="screen_signout_in_progress_dialog_content">"خارج ہورہاہے…"</string>
</resources>

View File

@@ -20,14 +20,14 @@ import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class LockScreenSettingsPresenterTest {
@Test
fun `present - remove pin option is hidden when mandatory`() = runTest {
val presenter = createLockScreenSettingsPresenter(this, lockScreenConfig = aLockScreenConfig(isPinMandatory = true))
val presenter = createLockScreenSettingsPresenter(lockScreenConfig = aLockScreenConfig(isPinMandatory = true))
presenter.test {
awaitItem().also { state ->
assertThat(state.showRemovePinOption).isFalse()
@@ -37,7 +37,7 @@ class LockScreenSettingsPresenterTest {
@Test
fun `present - remove pin flow`() = runTest {
val presenter = createLockScreenSettingsPresenter(this)
val presenter = createLockScreenSettingsPresenter()
presenter.test {
consumeItemsUntilPredicate { state ->
state.showRemovePinOption
@@ -71,7 +71,6 @@ class LockScreenSettingsPresenterTest {
isDeviceSecured = true,
)
val presenter = createLockScreenSettingsPresenter(
coroutineScope = this,
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
)
presenter.test {
@@ -88,7 +87,6 @@ class LockScreenSettingsPresenterTest {
}
)
val presenter = createLockScreenSettingsPresenter(
coroutineScope = this,
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
)
presenter.test {
@@ -110,7 +108,6 @@ class LockScreenSettingsPresenterTest {
}
)
val presenter = createLockScreenSettingsPresenter(
coroutineScope = this,
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
)
presenter.test {
@@ -130,7 +127,6 @@ class LockScreenSettingsPresenterTest {
)
val lockScreenStore = InMemoryLockScreenStore()
val presenter = createLockScreenSettingsPresenter(
coroutineScope = this,
lockScreenStore = lockScreenStore,
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
)
@@ -148,8 +144,7 @@ class LockScreenSettingsPresenterTest {
}
}
private suspend fun createLockScreenSettingsPresenter(
coroutineScope: CoroutineScope,
private suspend fun TestScope.createLockScreenSettingsPresenter(
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
@@ -160,7 +155,7 @@ class LockScreenSettingsPresenterTest {
return LockScreenSettingsPresenter(
lockScreenStore = lockScreenStore,
pinCodeManager = pinCodeManager,
coroutineScope = coroutineScope,
coroutineScope = this,
lockScreenConfig = lockScreenConfig,
biometricAuthenticatorManager = biometricAuthenticatorManager,
)

View File

@@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -34,7 +34,7 @@ class PinUnlockPresenterTest {
@Test
fun `present - success verify flow`() = runTest {
val presenter = createPinUnlockPresenter(this)
val presenter = createPinUnlockPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -71,7 +71,7 @@ class PinUnlockPresenterTest {
@Test
fun `present - failure verify flow`() = runTest {
val presenter = createPinUnlockPresenter(this)
val presenter = createPinUnlockPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -100,7 +100,7 @@ class PinUnlockPresenterTest {
fun `present - forgot pin flow`() = runTest {
val signOutLambda = lambdaRecorder<Boolean, Unit> {}
val signOut = FakeLogoutUseCase(signOutLambda)
val presenter = createPinUnlockPresenter(this, logoutUseCase = signOut)
val presenter = createPinUnlockPresenter(logoutUseCase = signOut)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -135,8 +135,7 @@ class PinUnlockPresenterTest {
dataOrNull()?.assertText(text)
}
private suspend fun createPinUnlockPresenter(
scope: CoroutineScope,
private suspend fun TestScope.createPinUnlockPresenter(
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }),
@@ -149,7 +148,7 @@ class PinUnlockPresenterTest {
pinCodeManager = pinCodeManager,
biometricAuthenticatorManager = biometricAuthenticatorManager,
logoutUseCase = logoutUseCase,
coroutineScope = scope,
coroutineScope = this,
pinUnlockHelper = PinUnlockHelper(biometricAuthenticatorManager, pinCodeManager),
)
}

View File

@@ -62,7 +62,7 @@ dependencies {
testImplementation(projects.features.enterprise.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.impl)
testImplementation(projects.libraries.oidc.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.tests.testutils)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)

View File

@@ -19,7 +19,7 @@ class DefaultLoginIntentResolver @Inject constructor() : LoginIntentResolver {
override fun parse(uriString: String): LoginParams? {
val uri = uriString.toUri()
if (uri.host != "mobile.element.io") return null
if (uri.path?.startsWith("/element")?.not() == true) return null
if (uri.path.orEmpty().startsWith("/element").not()) return null
val accountProvider = uri.getQueryParameter("account_provider") ?: return null
val loginHint = uri.getQueryParameter("login_hint")
return LoginParams(

View File

@@ -30,6 +30,7 @@ import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode
import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderNode
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode
import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
@@ -107,6 +108,9 @@ class LoginFlowNode @AssistedInject constructor(
val isAccountCreation: Boolean,
) : NavTarget
@Parcelize
data object ChooseAccountProvider : NavTarget
@Parcelize
data object ChangeAccountProvider : NavTarget
@@ -133,9 +137,13 @@ class LoginFlowNode @AssistedInject constructor(
)
}
override fun onSignIn() {
override fun onSignIn(mustChooseAccountProvider: Boolean) {
backstack.push(
if (mustChooseAccountProvider) {
NavTarget.ChooseAccountProvider
} else {
NavTarget.ConfirmAccountProvider(isAccountCreation = false)
}
)
}
@@ -166,6 +174,22 @@ class LoginFlowNode @AssistedInject constructor(
)
createNode<OnBoardingNode>(buildContext, listOf(callback, inputs))
}
NavTarget.ChooseAccountProvider -> {
val callback = object : ChooseAccountProviderNode.Callback {
override fun onOidcDetails(oidcDetails: OidcDetails) {
navigateToMas(oidcDetails)
}
override fun onCreateAccountContinue(url: String) {
backstack.push(NavTarget.CreateAccount(url))
}
override fun onLoginPasswordNeeded() {
backstack.push(NavTarget.LoginPassword)
}
}
createNode<ChooseAccountProviderNode>(buildContext, listOf(callback))
}
NavTarget.QrCode -> {
createNode<QrCodeLoginFlowNode>(buildContext)
}

View File

@@ -20,7 +20,8 @@ import javax.inject.Inject
class AccountProviderDataSource @Inject constructor(
enterpriseService: EnterpriseService,
) {
private val defaultAccountProvider = (enterpriseService.defaultHomeserverList().firstOrNull() ?: AuthenticationConfig.MATRIX_ORG_URL)
private val defaultAccountProvider =
(enterpriseService.defaultHomeserverList().firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } ?: AuthenticationConfig.MATRIX_ORG_URL)
.let { url ->
AccountProvider(
url = url,

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.accountprovider
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R
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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Text
/**
* https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817
*/
@Composable
fun AccountProviderOtherView(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.clickable { onClick() }
) {
HorizontalDivider()
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 44.dp)
.padding(vertical = 4.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RoundedIconAtom(
size = RoundedIconAtomSize.Medium,
imageVector = CompoundIcons.Search(),
tint = ElementTheme.colors.iconPrimary,
)
Text(
modifier = Modifier
.padding(start = 16.dp)
.weight(1f),
text = stringResource(R.string.screen_change_account_provider_other),
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textPrimary,
)
}
}
}
@PreviewsDayNight
@Composable
internal fun AccountProviderOtherViewPreview() = ElementPreview {
AccountProviderOtherView(
onClick = { },
)
}

View File

@@ -23,10 +23,14 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
fun anAccountProvider(
url: String = AuthenticationConfig.MATRIX_ORG_URL,
subtitle: String? = "Matrix.org is an open network for secure, decentralized communication.",
isPublic: Boolean = true,
isMatrixOrg: Boolean = true,
isValid: Boolean = true,
) = AccountProvider(
url = url,
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
isPublic = true,
isMatrixOrg = true,
isValid = true,
subtitle = subtitle,
isPublic = isPublic,
isMatrixOrg = isMatrixOrg,
isValid = isValid,
)

View File

@@ -39,6 +39,7 @@ fun AccountProviderView(
item: AccountProvider,
onClick: () -> Unit,
modifier: Modifier = Modifier,
selected: Boolean = false,
) {
Column(
modifier = modifier
@@ -66,7 +67,7 @@ fun AccountProviderView(
} else {
RoundedIconAtom(
size = RoundedIconAtomSize.Medium,
imageVector = CompoundIcons.Search(),
imageVector = CompoundIcons.Host(),
tint = ElementTheme.colors.iconPrimary,
)
}
@@ -88,6 +89,15 @@ fun AccountProviderView(
tint = ElementTheme.colors.iconSecondary,
)
}
if (selected) {
Icon(
modifier = Modifier
.padding(start = 10.dp),
imageVector = CompoundIcons.Check(),
contentDescription = null,
tint = ElementTheme.colors.iconAccentPrimary,
)
}
}
if (item.subtitle != null) {
Text(

View File

@@ -14,6 +14,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter
@@ -31,7 +32,8 @@ import javax.inject.Inject
/**
* This class is responsible for managing the login flow, including handling OIDC actions and
* submitting login requests.
* It's an helper to avoid code duplication. It is used by [OnBoardingPresenter] and [ConfirmAccountProviderPresenter].
* It's an helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter]
* and [ChooseAccountProviderPresenter].
*/
class LoginHelper @Inject constructor(
private val oidcActionFlow: OidcActionFlow,

View File

@@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.libraries.architecture.Presenter
@@ -25,6 +26,7 @@ class ChangeAccountProviderPresenter @Inject constructor(
override fun present(): ChangeAccountProviderState {
val staticAccountProviderList = remember {
enterpriseService.defaultHomeserverList()
.filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
.map { it.ensureProtocol() }
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
.map { url ->
@@ -38,9 +40,14 @@ class ChangeAccountProviderPresenter @Inject constructor(
}
}
val canSearchForAccountProviders = remember {
enterpriseService.canConnectToAnyHomeserver()
}
val changeServerState = changeServerPresenter.present()
return ChangeAccountProviderState(
accountProviders = staticAccountProviderList,
canSearchForAccountProviders = canSearchForAccountProviders,
changeServerState = changeServerState,
)
}

View File

@@ -13,5 +13,6 @@ import io.element.android.features.login.impl.changeserver.ChangeServerState
// Do not use default value, so no member get forgotten in the presenters.
data class ChangeAccountProviderState(
val accountProviders: List<AccountProvider>,
val canSearchForAccountProviders: Boolean,
val changeServerState: ChangeServerState,
)

View File

@@ -8,20 +8,28 @@
package io.element.android.features.login.impl.screens.changeaccountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.features.login.impl.changeserver.aChangeServerState
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
override val values: Sequence<ChangeAccountProviderState>
get() = sequenceOf(
aChangeAccountProviderState(),
aChangeAccountProviderState(canSearchForAccountProviders = false),
// Add other state here
)
}
fun aChangeAccountProviderState() = ChangeAccountProviderState(
accountProviders = listOf(
fun aChangeAccountProviderState(
accountProviders: List<AccountProvider> = listOf(
anAccountProvider()
),
changeServerState = aChangeServerState(),
canSearchForAccountProviders: Boolean = true,
changeServerState: ChangeServerState = aChangeServerState(),
) = ChangeAccountProviderState(
accountProviders = accountProviders,
canSearchForAccountProviders = canSearchForAccountProviders,
changeServerState = changeServerState,
)

View File

@@ -27,7 +27,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderOtherView
import io.element.android.features.login.impl.accountprovider.AccountProviderView
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
import io.element.android.features.login.impl.changeserver.ChangeServerView
@@ -95,13 +95,11 @@ fun ChangeAccountProviderView(
)
}
// Other
AccountProviderView(
item = AccountProvider(
url = "",
title = stringResource(id = R.string.screen_change_account_provider_other),
),
if (state.canSearchForAccountProviders) {
AccountProviderOtherView(
onClick = onOtherProviderClick
)
}
Spacer(Modifier.height(32.dp))
}
ChangeServerView(

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.screens.chooseaccountprovider
import io.element.android.features.login.impl.accountprovider.AccountProvider
sealed interface ChooseAccountProviderEvents {
data class SelectAccountProvider(val accountProvider: AccountProvider) : ChooseAccountProviderEvents
data object Continue : ChooseAccountProviderEvents
data object ClearError : ChooseAccountProviderEvents
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.screens.chooseaccountprovider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.OidcDetails
@ContributesNode(AppScope::class)
class ChooseAccountProviderNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: ChooseAccountProviderPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onLoginPasswordNeeded()
fun onOidcDetails(oidcDetails: OidcDetails)
fun onCreateAccountContinue(url: String)
}
private fun onOidcDetails(oidcDetails: OidcDetails) {
plugins<Callback>().forEach { it.onOidcDetails(oidcDetails) }
}
private fun onLoginPasswordNeeded() {
plugins<Callback>().forEach { it.onLoginPasswordNeeded() }
}
private fun onCreateAccountContinue(url: String) {
plugins<Callback>().forEach { it.onCreateAccountContinue(url) }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val context = LocalContext.current
ChooseAccountProviderView(
state = state,
modifier = modifier,
onBackClick = ::navigateUp,
onOidcDetails = ::onOidcDetails,
onNeedLoginPassword = ::onLoginPasswordNeeded,
onLearnMoreClick = { openLearnMorePage(context) },
onCreateAccountContinue = ::onCreateAccountContinue,
)
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.screens.chooseaccountprovider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.login.LoginHelper
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.uri.ensureProtocol
import javax.inject.Inject
class ChooseAccountProviderPresenter @Inject constructor(
private val enterpriseService: EnterpriseService,
private val loginHelper: LoginHelper,
) : Presenter<ChooseAccountProviderState> {
@Composable
override fun present(): ChooseAccountProviderState {
val localCoroutineScope = rememberCoroutineScope()
val loginMode by loginHelper.collectLoginMode()
var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) }
fun handleEvent(event: ChooseAccountProviderEvents) {
when (event) {
ChooseAccountProviderEvents.Continue -> {
selectedAccountProvider?.let {
loginHelper.submit(
coroutineScope = localCoroutineScope,
isAccountCreation = false,
homeserverUrl = it.url,
loginHint = null,
)
}
}
is ChooseAccountProviderEvents.SelectAccountProvider -> {
// Ensure that the user do not change the server during processing
if (loginMode is AsyncData.Uninitialized) {
selectedAccountProvider = event.accountProvider
}
}
ChooseAccountProviderEvents.ClearError -> loginHelper.clearError()
}
}
val staticAccountProviderList = remember {
// The list cannot contains ANY_ACCOUNT_PROVIDER ("*") and cannot be empty at this point
enterpriseService.defaultHomeserverList()
.map { it.ensureProtocol() }
.map { url ->
AccountProvider(
url = url,
subtitle = null,
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
isValid = true,
)
}
}
return ChooseAccountProviderState(
accountProviders = staticAccountProviderList,
selectedAccountProvider = selectedAccountProvider,
loginMode = loginMode,
eventSink = ::handleEvent,
)
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.screens.chooseaccountprovider
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
// Do not use default value, so no member get forgotten in the presenters.
data class ChooseAccountProviderState(
val accountProviders: List<AccountProvider>,
val selectedAccountProvider: AccountProvider?,
val loginMode: AsyncData<LoginMode>,
val eventSink: (ChooseAccountProviderEvents) -> Unit,
) {
val submitEnabled: Boolean
get() = selectedAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.screens.chooseaccountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
open class ChooseAccountProviderStateProvider : PreviewParameterProvider<ChooseAccountProviderState> {
private val server1 = anAccountProvider(
url = "https://server1.io",
subtitle = null,
isPublic = false,
isMatrixOrg = false,
)
private val server2 = anAccountProvider(
url = "https://server2.io",
subtitle = null,
isPublic = false,
isMatrixOrg = false,
)
private val server3 = anAccountProvider(
url = "https://server3.io",
subtitle = null,
isPublic = false,
isMatrixOrg = false,
)
override val values: Sequence<ChooseAccountProviderState>
get() = sequenceOf(
aChooseAccountProviderState(
accountProviders = listOf(
server1,
server2,
server3,
)
),
aChooseAccountProviderState(
accountProviders = listOf(
server1,
server2,
server3,
),
selectedAccountProvider = server2,
),
aChooseAccountProviderState(
accountProviders = listOf(
server1,
server2,
server3,
),
selectedAccountProvider = server2,
loginMode = AsyncData.Loading(),
),
// Add other state here
)
}
fun aChooseAccountProviderState(
accountProviders: List<AccountProvider> = listOf(
anAccountProvider()
),
selectedAccountProvider: AccountProvider? = null,
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
eventSink: (ChooseAccountProviderEvents) -> Unit = {},
) = ChooseAccountProviderState(
accountProviders = accountProviders,
selectedAccountProvider = selectedAccountProvider,
loginMode = loginMode,
eventSink = eventSink,
)

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.features.login.impl.screens.chooseaccountprovider
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.accountprovider.AccountProviderView
import io.element.android.features.login.impl.login.LoginModeView
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun ChooseAccountProviderView(
state: ChooseAccountProviderState,
onBackClick: () -> Unit,
onOidcDetails: (OidcDetails) -> Unit,
onNeedLoginPassword: () -> Unit,
onLearnMoreClick: () -> Unit,
onCreateAccountContinue: (url: String) -> Unit,
modifier: Modifier = Modifier,
) {
val isLoading by remember(state.loginMode) {
derivedStateOf {
state.loginMode is AsyncData.Loading
}
}
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
title = {},
navigationIcon = { BackButton(onClick = onBackClick) }
)
}
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.imePadding()
.padding(padding)
.consumeWindowInsets(padding)
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = rememberScrollState())
) {
IconTitleSubtitleMolecule(
modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp),
iconStyle = BigIcon.Style.Default(CompoundIcons.HomeSolid()),
title = stringResource(id = R.string.screen_server_confirmation_title_picker_mode),
subTitle = null,
)
state.accountProviders.forEach { item ->
val alteredItem = if (item.isMatrixOrg) {
// Set the subtitle from the resource
item.copy(
subtitle = stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle),
)
} else {
item
}
AccountProviderView(
item = alteredItem,
selected = item == state.selectedAccountProvider,
onClick = {
state.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(item))
}
)
}
Spacer(Modifier.height(32.dp))
// Flexible spacing to keep the submit button at the bottom
Spacer(modifier = Modifier.weight(1f))
Button(
text = stringResource(id = CommonStrings.action_continue),
showProgress = isLoading,
onClick = {
state.eventSink(ChooseAccountProviderEvents.Continue)
},
enabled = state.submitEnabled || isLoading,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(48.dp))
}
LoginModeView(
loginMode = state.loginMode,
onClearError = {
state.eventSink(ChooseAccountProviderEvents.ClearError)
},
onLearnMoreClick = onLearnMoreClick,
onOidcDetails = onOidcDetails,
onNeedLoginPassword = onNeedLoginPassword,
onCreateAccountContinue = onCreateAccountContinue,
)
}
}
}
@PreviewsDayNight
@Composable
internal fun ChooseAccountProviderViewPreview(@PreviewParameter(ChooseAccountProviderStateProvider::class) state: ChooseAccountProviderState) = ElementPreview {
ChooseAccountProviderView(
state = state,
onBackClick = { },
onLearnMoreClick = { },
onOidcDetails = { },
onNeedLoginPassword = { },
onCreateAccountContinue = { },
)
}

View File

@@ -34,7 +34,7 @@ class OnBoardingNode @AssistedInject constructor(
) {
interface Callback : Plugin {
fun onSignUp()
fun onSignIn()
fun onSignIn(mustChooseAccountProvider: Boolean)
fun onSignInWithQrCode()
fun onReportProblem()
fun onLoginPasswordNeeded()
@@ -53,8 +53,8 @@ class OnBoardingNode @AssistedInject constructor(
params = params,
)
private fun onSignIn() {
plugins<Callback>().forEach { it.onSignIn() }
private fun onSignIn(mustChooseAccountProvider: Boolean) {
plugins<Callback>().forEach { it.onSignIn(mustChooseAccountProvider) }
}
private fun onSignUp() {

View File

@@ -16,6 +16,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.appconfig.OnBoardingConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
import io.element.android.features.login.impl.login.LoginHelper
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter
@@ -27,6 +29,7 @@ class OnBoardingPresenter @AssistedInject constructor(
@Assisted private val params: OnBoardingNode.Params,
private val buildMeta: BuildMeta,
private val featureFlagService: FeatureFlagService,
private val enterpriseService: EnterpriseService,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val loginHelper: LoginHelper,
) : Presenter<OnBoardingState> {
@@ -37,15 +40,33 @@ class OnBoardingPresenter @AssistedInject constructor(
): OnBoardingPresenter
}
private val defaultAccountProvider = params.accountProvider
private val loginHint = params.loginHint
@Composable
override fun present(): OnBoardingState {
val localCoroutineScope = rememberCoroutineScope()
val canLoginWithQrCode by produceState(initialValue = false) {
value = defaultAccountProvider == null &&
val forcedAccountProvider = remember {
// If defaultHomeserverList() returns a singleton list, this is the default account provider.
// In this case, the user can sign in using this homeserver, or use QrCode login
enterpriseService.defaultHomeserverList().singleOrNull()
}
val canConnectToAnyHomeserver = remember {
enterpriseService.canConnectToAnyHomeserver()
}
val mustChooseAccountProvider = remember {
!canConnectToAnyHomeserver && enterpriseService.defaultHomeserverList().size > 1
}
val linkAccountProvider by produceState<String?>(initialValue = null) {
// Account provider from the link, if allowed by the enterprise service
value = params.accountProvider?.takeIf {
enterpriseService.isAllowedToConnectToHomeserver(it)
}
}
val defaultAccountProvider = remember(linkAccountProvider) {
// If there is a forced account provider, this is the default account provider
// Else use the account provider passed in the params if any and if allowed
forcedAccountProvider ?: linkAccountProvider
}
val canLoginWithQrCode by produceState(initialValue = false, linkAccountProvider) {
value = linkAccountProvider == null &&
featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
}
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
@@ -58,7 +79,7 @@ class OnBoardingPresenter @AssistedInject constructor(
coroutineScope = localCoroutineScope,
isAccountCreation = false,
homeserverUrl = event.defaultAccountProvider,
loginHint = loginHint,
loginHint = params.loginHint?.takeIf { forcedAccountProvider == null },
)
OnBoardingEvents.ClearError -> loginHelper.clearError()
}
@@ -67,8 +88,9 @@ class OnBoardingPresenter @AssistedInject constructor(
return OnBoardingState(
productionApplicationName = buildMeta.productionApplicationName,
defaultAccountProvider = defaultAccountProvider,
mustChooseAccountProvider = mustChooseAccountProvider,
canLoginWithQrCode = canLoginWithQrCode,
canCreateAccount = defaultAccountProvider == null && OnBoardingConfig.CAN_CREATE_ACCOUNT,
canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT,
canReportBug = canReportBug,
loginMode = loginMode,
eventSink = ::handleEvent,

View File

@@ -13,6 +13,7 @@ import io.element.android.libraries.architecture.AsyncData
data class OnBoardingState(
val productionApplicationName: String,
val defaultAccountProvider: String?,
val mustChooseAccountProvider: Boolean,
val canLoginWithQrCode: Boolean,
val canCreateAccount: Boolean,
val canReportBug: Boolean,

View File

@@ -26,6 +26,7 @@ open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
fun anOnBoardingState(
productionApplicationName: String = "Element",
defaultAccountProvider: String? = null,
mustChooseAccountProvider: Boolean = false,
canLoginWithQrCode: Boolean = false,
canCreateAccount: Boolean = false,
canReportBug: Boolean = false,
@@ -34,6 +35,7 @@ fun anOnBoardingState(
) = OnBoardingState(
productionApplicationName = productionApplicationName,
defaultAccountProvider = defaultAccountProvider,
mustChooseAccountProvider = mustChooseAccountProvider,
canLoginWithQrCode = canLoginWithQrCode,
canCreateAccount = canCreateAccount,
canReportBug = canReportBug,

View File

@@ -56,7 +56,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun OnBoardingView(
state: OnBoardingState,
onSignInWithQrCode: () -> Unit,
onSignIn: () -> Unit,
onSignIn: (mustChooseAccountProvider: Boolean) -> Unit,
onCreateAccount: () -> Unit,
onOidcDetails: (OidcDetails) -> Unit,
onNeedLoginPassword: () -> Unit,
@@ -143,7 +143,7 @@ private fun OnBoardingContent(state: OnBoardingState) {
private fun OnBoardingButtons(
state: OnBoardingState,
onSignInWithQrCode: () -> Unit,
onSignIn: () -> Unit,
onSignIn: (mustChooseAccountProvider: Boolean) -> Unit,
onCreateAccount: () -> Unit,
onReportProblem: () -> Unit,
) {
@@ -171,7 +171,9 @@ private fun OnBoardingButtons(
if (defaultAccountProvider == null) {
Button(
text = stringResource(id = signInButtonStringRes),
onClick = onSignIn,
onClick = {
onSignIn(state.mustChooseAccountProvider)
},
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.onBoardingSignIn)

View File

@@ -89,5 +89,6 @@ Zkuste se přihlásit ručně nebo naskenujte QR kód pomocí jiného zařízen
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci."</string>
<string name="screen_server_confirmation_message_register">"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_server_confirmation_title_login">"Chystáte se přihlásit do služby %1$s"</string>
<string name="screen_server_confirmation_title_picker_mode">"Vyberte poskytovatele účtu"</string>
<string name="screen_server_confirmation_title_register">"Chystáte se vytvořit účet na %1$s"</string>
</resources>

View File

@@ -89,5 +89,6 @@ Ceisiwch fewngofnodi â llaw, neu sganiwch y cod QR gyda dyfais arall."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Mae Matrix yn rhwydwaith agored ar gyfer cyfathrebu diogel, datganoledig."</string>
<string name="screen_server_confirmation_message_register">"Dyma lle bydd eich sgyrsiau\'n byw - yn union fel y byddech chi\'n defnyddio darparwr e-bost i gadw\'ch e-byst."</string>
<string name="screen_server_confirmation_title_login">"Rydych ar fin mewngofnodi i %1$s"</string>
<string name="screen_server_confirmation_title_picker_mode">"Dewiswch ddarparwr cyfrif"</string>
<string name="screen_server_confirmation_title_register">"Rydych chi ar fin creu cyfrif ar %1$s"</string>
</resources>

View File

@@ -6,18 +6,20 @@
<string name="screen_account_provider_form_subtitle">"Suche nach einem Unternehmen, einer Community oder einem privaten Server."</string>
<string name="screen_account_provider_form_title">"Kontoanbieter finden"</string>
<string name="screen_account_provider_signin_subtitle">"Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden."</string>
<string name="screen_account_provider_signin_title">"Du bist dabei, dich bei %s anzumelden"</string>
<string name="screen_account_provider_signin_title">"Sie sind dabei, sich bei %s anzumelden"</string>
<string name="screen_account_provider_signup_subtitle">"Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden."</string>
<string name="screen_account_provider_signup_title">"Du bist dabei, ein Konto bei %s zu erstellen"</string>
<string name="screen_account_provider_signup_title">"Sie sind dabei, ein Konto bei %s zu erstellen."</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org ist ein großer, kostenloser Server im öffentlichen Matrix-Netzwerk für eine sichere, dezentralisierte Kommunikation, der von der Matrix.org Foundation betrieben wird."</string>
<string name="screen_change_account_provider_other">"Sonstige"</string>
<string name="screen_change_account_provider_subtitle">"Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Geschäftskonto."</string>
<string name="screen_change_account_provider_title">"Kontoanbieter wechseln"</string>
<string name="screen_change_server_error_invalid_homeserver">"Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfe, ob du die Homeserver-URL korrekt eingegeben hast. Wenn die URL korrekt ist, wende dich an deinen Homeserver-Administrator, um weitere Hilfe zu erhalten."</string>
<string name="screen_change_server_error_invalid_well_known">"Der Server ist aufgrund eines Problems im \"well-known file\" nicht verfügbar:
<string name="screen_change_server_error_invalid_homeserver">"Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfen Sie ob die Homeserver-URL korrekt eingegeben wurde. Wenn die URL korrekt ist, wenden Sie sich an ihren Homeserver- Administrator, um weitere Hilfe zu erhalten."</string>
<string name="screen_change_server_error_invalid_well_known">"Der Server ist aufgrund eines Problems in der \".well-known\" Datei nicht verfügbar:
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Der gewählte Kontoanbieter unterstützt Sliding Sync nicht. Für die Verwendung von %1$s ist ein Upgrade des Servers erforderlich."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s darf keine Verbindung herstellen zu %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s darf keine Verbindung zu %2$s herstellen."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"Diese App wurde so konfiguriert, dass sie %1$s zulässt."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Kontoanbieter %1$s ist nicht zulässig."</string>
<string name="screen_change_server_form_header">"Homeserver-URL"</string>
<string name="screen_change_server_form_notice">"Geben Sie eine Domainadresse ein."</string>
<string name="screen_change_server_subtitle">"Wie lautet die Adresse deines Servers?"</string>
@@ -33,6 +35,7 @@
<string name="screen_login_title">"Willkommen zurück!"</string>
<string name="screen_login_title_with_homeserver">"Anmelden bei %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Manuell anmelden"</string>
<string name="screen_onboarding_sign_in_to">"Bei %1$s anmelden"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Mit QR-Code anmelden"</string>
<string name="screen_onboarding_sign_up">"Konto erstellen"</string>
<string name="screen_onboarding_welcome_message">"Willkommen beim schnellsten %1$s aller Zeiten. Optimiert für Geschwindigkeit und Einfachheit."</string>
@@ -45,7 +48,7 @@
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Wenn das Problem bestehen bleibt, versuche es mit einem anderen WLAN-Netzwerk oder verwende deine mobilen Daten statt WLAN."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Wenn das nicht funktioniert, melde dich manuell an"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"Die Verbindung ist nicht sicher"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Du wirst aufgefordert, die beiden unten abgebildeten Ziffern einzugeben."</string>
<string name="screen_qr_code_login_device_code_subtitle">"Sie werden aufgefordert, die beiden auf diesem Gerät angezeigten Ziffern einzugeben."</string>
<string name="screen_qr_code_login_device_code_title">"Trage die unten angezeigte Zahl auf einem anderen Device ein"</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"Melde dich auf deinem anderen Gerät an und versuche es dann noch einmal oder verwende ein anderes Gerät, das bereits angemeldet ist."</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Anderes Gerät ist nicht angemeldet"</string>
@@ -73,7 +76,7 @@ Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Ger
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Falscher QR-Code"</string>
<string name="screen_qr_code_login_no_camera_permission_button">"Gehe zu den Kameraeinstellungen"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"Du musst %1$s die Erlaubnis erteilen, die Kamera deines Geräts zu verwenden, um fortzufahren."</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"Sie müssen %1$s die Erlaubnis erteilen, die Kamera Ihres Geräts zu verwenden um fortzufahren."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"Erlaube Zugriff auf die Kamera zum Scannen des QR-Codes"</string>
<string name="screen_qr_code_login_scanning_state_title">"QR-Code scannen"</string>
<string name="screen_qr_code_login_start_over_button">"Neu beginnen"</string>
@@ -85,6 +88,7 @@ Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Ger
<string name="screen_server_confirmation_message_login_element_dot_io">"Ein privater Server für die Mitarbeiter von Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."</string>
<string name="screen_server_confirmation_message_register">"Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden."</string>
<string name="screen_server_confirmation_title_login">"Du bist dabei, dich bei %1$s anzumelden"</string>
<string name="screen_server_confirmation_title_register">"Du bist dabei, ein Konto auf %1$s zu erstellen"</string>
<string name="screen_server_confirmation_title_login">"Sie sind dabei, sich bei %1$s anzumelden"</string>
<string name="screen_server_confirmation_title_picker_mode">"Kontoanbieter auswählen"</string>
<string name="screen_server_confirmation_title_register">"Sie sind dabei, ein Konto auf %1$s zu erstellen"</string>
</resources>

View File

@@ -89,5 +89,6 @@ Proovi käsitsi sisselogimist või skaneeri QR-koodi mõne muu seadmega."</strin
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix on avatud võrk turvalise ja hajutatud suhtluse jaoks."</string>
<string name="screen_server_confirmation_message_register">"See on koht, kus sinu vestlused elavad just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."</string>
<string name="screen_server_confirmation_title_login">"Sa oled sisselogimas koduserverisse %1$s"</string>
<string name="screen_server_confirmation_title_picker_mode">"Vali teenusepakkuja"</string>
<string name="screen_server_confirmation_title_register">"Sa oled loomas kasutajakontot koduserveris %1$s"</string>
</resources>

View File

@@ -11,6 +11,7 @@
<string name="screen_change_account_provider_subtitle">"Erabili beste kontu-hornitzaile bat, hala nola zure zerbitzari pribatua edo laneko kontu bat."</string>
<string name="screen_change_account_provider_title">"Aldatu kontu-hornitzailea"</string>
<string name="screen_change_server_form_header">"Zerbitzariaren URLa"</string>
<string name="screen_change_server_form_notice">"Sartu domeinu-helbide bat."</string>
<string name="screen_change_server_subtitle">"Zein da zure zerbitzariaren helbidea?"</string>
<string name="screen_change_server_title">"Hautatu zure zerbitzaria"</string>
<string name="screen_create_account_title">"Sortu kontua"</string>
@@ -29,6 +30,7 @@
<string name="screen_qr_code_login_connecting_subtitle">"Konexio segurua ezartzen"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Ezin izan da konexio segururik ezarri gailu berriarekin. Lehendik dauden gailuak seguru daude oraindik ere eta ez duzu haietaz kezkatu beharrik."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Orain zer?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Saiatu berriro QR kodearekin saioa hasten sare-arazo bat izan bada"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Horrek ez badu funtzionatzen, hasi saioa eskuz"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"Konexioa ez da segurua"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Gailu honetan agertzen diren bi digituak sartzeko eskatuko zaizu."</string>
@@ -36,6 +38,7 @@
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"Hasi saioa beste gailu batean eta saiatu berriro, edo erabili saioa hasita duen beste gailuren bat."</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Saioa hasteko eskaera bertan behera utzi da beste gailuan"</string>
<string name="screen_qr_code_login_error_cancelled_title">"Saioa hasteko eskaera bertan behera utzi da"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Saioa hasteari uko egin zaio beste dispositiboan."</string>
<string name="screen_qr_code_login_error_declined_title">"Saio-hasiera ukatu da"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Saio-hasiera iraungi da. Saiatu berriro."</string>
<string name="screen_qr_code_login_error_expired_title">"Saio-hasiera ez da garaiz gauzatu."</string>

View File

@@ -14,7 +14,7 @@
<string name="screen_change_account_provider_subtitle">"Käytä toista palveluntarjoajaa, kuten omaa yksityistä palvelintasi tai työpaikkaasi."</string>
<string name="screen_change_account_provider_title">"Vaihda palveluntarjoajaa"</string>
<string name="screen_change_server_error_invalid_homeserver">"Kotipalvelimeen ei saatu yhteyttä. Varmista, että olet syöttänyt osoitteen oikein. Jos osoite on oikein, ota yhteyttä palvelimesi ylläpitäjään."</string>
<string name="screen_change_server_error_invalid_well_known">"Sliding sync ei ole saatavilla well-known tiedostossa olevan ongelman vuoksi:
<string name="screen_change_server_error_invalid_well_known">"Palvelin ei ole saatavilla .well-known tiedostossa olevan ongelman vuoksi:
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Valitsemasi palveluntarjoaja ei tue sliding syncia. Palvelimen päivitys tarvitaan %1$s -sovelluksen käyttämiseen."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s ei saa yhdistää %2$s -palvelimeen."</string>
@@ -89,5 +89,6 @@ Yritä kirjautua sisään manuaalisesti tai skannaa QR-koodi toisella laitteella
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix on avoin verkko turvallista, hajautettua viestintää varten."</string>
<string name="screen_server_confirmation_message_register">"Keskustelusi asuvat täällä — aivan kuten aivan kuten käyttäisit sähköpostipalveluntarjoajaa sähköpostiesi säilyttämiseen."</string>
<string name="screen_server_confirmation_title_login">"Olet kirjautumassa sisään %1$s-palvelimelle"</string>
<string name="screen_server_confirmation_title_picker_mode">"Valitse palveluntarjoaja"</string>
<string name="screen_server_confirmation_title_register">"Olet luomassa tiliä %1$s-palvelimelle"</string>
</resources>

View File

@@ -87,5 +87,6 @@
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."</string>
<string name="screen_server_confirmation_message_register">"Cest ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails."</string>
<string name="screen_server_confirmation_title_login">"Vous êtes sur le point de vous connecter à %1$s"</string>
<string name="screen_server_confirmation_title_picker_mode">"Choisissez un fournisseur de compte"</string>
<string name="screen_server_confirmation_title_register">"Vous êtes sur le point de créer un compte sur %1$s"</string>
</resources>

View File

@@ -89,5 +89,6 @@ Próbáljon meg kézileg bejelentkezni, vagy olvassa be a QR-kódot egy másik e
<string name="screen_server_confirmation_message_login_matrix_dot_org">"A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz."</string>
<string name="screen_server_confirmation_message_register">"Itt lesznek a beszélgetései ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez."</string>
<string name="screen_server_confirmation_title_login">"Hamarosan bejelentkezik ebbe: %1$s"</string>
<string name="screen_server_confirmation_title_picker_mode">"Válassza ki a fiókszolgáltatót"</string>
<string name="screen_server_confirmation_title_register">"Hamarosan létrehoz egy fiókot ezen: %1$s"</string>
</resources>

View File

@@ -14,7 +14,9 @@
<string name="screen_change_account_provider_subtitle">"Use um provedor de conta diferente, como seu próprio servidor privado ou uma conta corporativa."</string>
<string name="screen_change_account_provider_title">"Alterar provedor da conta"</string>
<string name="screen_change_server_error_invalid_homeserver">"Não conseguimos acessar esse servidor. Verifique se você inseriu a URL do servidor corretamente. Se a URL estiver correta, entre em contato com o administrador do servidor para obter mais ajuda."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"O provedor de conta selecionado não é compatível com a sliding sync. É necessária uma atualização do servidor para que você possa usar o %1$s."</string>
<string name="screen_change_server_form_header">"URL do servidor"</string>
<string name="screen_change_server_form_notice">"Insira um endereço de domínio."</string>
<string name="screen_change_server_subtitle">"Qual é o endereço do seu servidor?"</string>
<string name="screen_change_server_title">"Selecione seu servidor"</string>
<string name="screen_create_account_title">"Criar conta"</string>
@@ -33,8 +35,48 @@
<string name="screen_onboarding_welcome_message">"Bem-vindo ao mais rápido %1$s de todos os tempos. Turbinado para velocidade e simplicidade."</string>
<string name="screen_onboarding_welcome_subtitle">"Bem-vindo ao %1$s. Turbinado, para velocidade e simplicidade"</string>
<string name="screen_onboarding_welcome_title">"Esteja no seu elemento"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Estabelecendo uma conexão segura"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Não foi possível estabelecer uma conexão segura com o novo dispositivo. Seus dispositivos existentes ainda estão seguros e você não precisa se preocupar com eles."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"E agora?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Tente iniciar sessão novamente com um código QR caso este tenha sido um problema de rede"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Se o problema persistir, tente uma rede Wi-Fi diferente ou use seus dados móveis em vez de Wi-Fi"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Se isso não funcionar, faça login manualmente"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"Conexão não segura"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Você será solicitado a inserir os dois dígitos mostrados neste dispositivo."</string>
<string name="screen_qr_code_login_device_code_title">"Digite o número abaixo em seu outro dispositivo"</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"Faça login em seu outro dispositivo e tente novamente, ou use outro dispositivo que já esteja conectado."</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Outro dispositivo não conectado"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"O login foi cancelado no outro dispositivo."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Solicitação de login cancelada"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"O login foi recusado no outro dispositivo."</string>
<string name="screen_qr_code_login_error_declined_title">"Login recusado"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"O login expirou. Tente novamente."</string>
<string name="screen_qr_code_login_error_expired_title">"O login não foi concluído a tempo"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Seu outro dispositivo não é compatível com o login em %s com um código QR.
Tente fazer login manualmente ou escanear o código QR com outro dispositivo."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"Código QR incompatível"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Seu provedor de conta não é compatível com %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s incompatível"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Pronto para escanear"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Abrir %1$s em um dispositivo desktop"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Clique no seu avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Selecione %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Vincular novo dispositivo\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Leia o código QR com este dispositivo"</string>
<string name="screen_qr_code_login_initial_state_subtitle">"Disponível somente se o provedor da sua conta for compatível."</string>
<string name="screen_qr_code_login_initial_state_title">"Abra %1$s em outro dispositivo para obter o código QR"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Use o código QR exibido no outro dispositivo."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Tente novamente"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Código QR errado"</string>
<string name="screen_qr_code_login_no_camera_permission_button">"Ir para as configurações da câmera"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"Você deve permitir ao %1$s usar a câmera do seu dispositivo para continuar."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"Permita o acesso à câmera para escanear o código QR"</string>
<string name="screen_qr_code_login_scanning_state_title">"Leia o código QR"</string>
<string name="screen_qr_code_login_start_over_button">"Comece de novo"</string>
<string name="screen_qr_code_login_unknown_error_description">"Ocorreu um erro inesperado. Tente novamente."</string>
<string name="screen_qr_code_login_verify_code_loading">"Aguardando seu outro dispositivo"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"Seu provedor de conta pode solicitar o seguinte código para verificar o login."</string>
<string name="screen_qr_code_login_verify_code_title">"Seu código de verificação"</string>
<string name="screen_server_confirmation_change_server">"Alterar provedor da conta"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Um servidor privado para funcionários do Element."</string>

View File

@@ -14,9 +14,14 @@
<string name="screen_change_account_provider_subtitle">"Utiliza um operador de conta diferente, como o teu próprio servidor privado ou uma conta de trabalho."</string>
<string name="screen_change_account_provider_title">"Alterar operador de conta"</string>
<string name="screen_change_server_error_invalid_homeserver">"Não foi possível comunicar com este servidor. Por favor, verifica se introduziste o seu URL corretamente. Se sim, contacta o administrador para obteres mais ajuda."</string>
<string name="screen_change_server_error_invalid_well_known">"A sincronização deslizante (sliding sync) não está disponível devido a um problema no ficheiro \"well-known\":
<string name="screen_change_server_error_invalid_well_known">"O servidor não está disponível devido a um problema no ficheiro \".well-known\":
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"O operador de conta selecionado não suporta sincronização deslizante (sliding sync). É necessária uma atualização do servidor para poder usar a %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s não se pode ligar a %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"Esta aplicação foi configurada para permitir: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Operador de conta %1$s não permitido."</string>
<string name="screen_change_server_form_header">"URL do servidor"</string>
<string name="screen_change_server_form_notice">"Insere um endereço"</string>
<string name="screen_change_server_subtitle">"Qual é o endereço do teu servidor?"</string>
<string name="screen_change_server_title">"Seleciona o teu servidor"</string>
<string name="screen_create_account_title">"Criar conta"</string>
@@ -30,6 +35,7 @@
<string name="screen_login_title">"Bem-vindo(a) de volta!"</string>
<string name="screen_login_title_with_homeserver">"Iniciar sessão em %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Iniciar sessão manualmente"</string>
<string name="screen_onboarding_sign_in_to">"Iniciar sessão em %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Iniciar sessão com código QR"</string>
<string name="screen_onboarding_sign_up">"Criar conta"</string>
<string name="screen_onboarding_welcome_message">"Bem-vindo(a) à %1$s mais rápida de sempre. Super rápida e simples."</string>
@@ -83,5 +89,6 @@ Tenta iniciar a sessão manualmente ou digitaliza o código QR com outro disposi
<string name="screen_server_confirmation_message_login_matrix_dot_org">"A Matrix é uma rede aberta de comunicação descentralizada e segura."</string>
<string name="screen_server_confirmation_message_register">"É aqui que as tuas conversas vão ficar — tal como num serviço de e-mail."</string>
<string name="screen_server_confirmation_title_login">"Irás iniciar sessão em %1$s"</string>
<string name="screen_server_confirmation_title_picker_mode">"Escolher operador de conta"</string>
<string name="screen_server_confirmation_title_register">"Irás criar uma conta em %1$s"</string>
</resources>

View File

@@ -89,5 +89,6 @@ Skúste sa prihlásiť manuálne alebo naskenujte QR kód pomocou iného zariade
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."</string>
<string name="screen_server_confirmation_message_register">"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_server_confirmation_title_login">"Chystáte sa prihlásiť do %1$s"</string>
<string name="screen_server_confirmation_title_picker_mode">"Vyberte poskytovateľa účtu"</string>
<string name="screen_server_confirmation_title_register">"Chystáte sa vytvoriť účet na %1$s"</string>
</resources>

View File

@@ -14,9 +14,14 @@
<string name="screen_change_account_provider_subtitle">"Använd en annan kontoleverantör, till exempel din egen privata server eller ett jobbkonto."</string>
<string name="screen_change_account_provider_title">"Byt kontoleverantör"</string>
<string name="screen_change_server_error_invalid_homeserver">"Vi kunde inte nå den här hemservern. Kontrollera att du har angett hemserverns URL korrekt. Om URL:en är korrekt kontaktar du administratören för hemservern för ytterligare hjälp."</string>
<string name="screen_change_server_error_invalid_well_known">"Sliding Sync är inte tillgängligt på grund av ett problem i well-known-filen:
<string name="screen_change_server_error_invalid_well_known">"Sliding Sync är inte tillgängligt på grund av ett problem i .well-known-filen:
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Den valda kontoleverantören stöder inte sliding sync. En uppgradering till servern behövs för att använda %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s får inte ansluta till %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"Den här appen har konfigurerats för att tillåta: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Kontoleverantör %1$s är inte tillåten."</string>
<string name="screen_change_server_form_header">"Hemserverns URL"</string>
<string name="screen_change_server_form_notice">"Ange en domänadress."</string>
<string name="screen_change_server_subtitle">"Vad är adressen till din server?"</string>
<string name="screen_change_server_title">"Välj din server"</string>
<string name="screen_create_account_title">"Skapa konto"</string>
@@ -30,6 +35,7 @@
<string name="screen_login_title">"Välkommen tillbaka!"</string>
<string name="screen_login_title_with_homeserver">"Logga in på %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Logga in manuellt"</string>
<string name="screen_onboarding_sign_in_to">"Logga in på %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Logga in med QR-kod"</string>
<string name="screen_onboarding_sign_up">"Skapa konto"</string>
<string name="screen_onboarding_welcome_message">"Välkommen till den snabbaste %1$s någonsin. Superladdad för snabbhet och enkelhet."</string>
@@ -83,5 +89,6 @@ Prova att logga in manuellt eller skanna QR-koden med en annan enhet."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string>
<string name="screen_server_confirmation_message_register">"Det är här dina konversationer kommer att sparas - precis som du skulle använda en e-postleverantör för att spara dina e-brev."</string>
<string name="screen_server_confirmation_title_login">"Du är på väg att logga in på %1$s"</string>
<string name="screen_server_confirmation_title_picker_mode">"Välj kontoleverantör"</string>
<string name="screen_server_confirmation_title_register">"Du är på väg att skapa ett konto på %1$s"</string>
</resources>

View File

@@ -16,6 +16,10 @@
<string name="screen_change_server_error_invalid_homeserver">"Не вдалося під\'єднатися до цього домашнього сервера. Перевірте правильність введеної URL-адреси домашнього сервера. Якщо URL-адреса правильна, зверніться по додаткову допомогу до адміністратора домашнього сервера."</string>
<string name="screen_change_server_error_invalid_well_known">"Сервер недоступний через помилку у файлі well-known:
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Вибраний постачальник облікових записів не підтримує sliding sync. Щоб користуватися %1$s необхідно оновити сервер."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s не дозволено під\'єднуватися до %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"Цей застосунок налаштовано так, щоб дозволити: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Постачальник облікового запису %1$s не дозволений."</string>
<string name="screen_change_server_form_header">"URL-адреса домашнього сервера"</string>
<string name="screen_change_server_form_notice">"Введіть адресу домену."</string>
<string name="screen_change_server_subtitle">"Яка адреса вашого сервера?"</string>

View File

@@ -0,0 +1,86 @@
<?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_matrix_org_subtitle">"Matrix.org Matrix.org فاؤنڈیشن کے ذریعہ محفوظ، غیر مرکزی مواصلات کے لئے عوامی میٹرکس شبکہ پر ایک بڑا، مفت خادم ہے۔"</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">"ہم اس منزلی خادم تک نہیں پہنچ سکے۔ برائے مہربانی پڑتال کریں کہ آپ نے ہوم سرور کا عنوان صحیح طریقے سے درج کیا ہے۔ اگر عنوان درست ہے تو مزید مدد کے لیے اپنے منزلی خادم کے منتظم سے رابطہ کریں۔"</string>
<string name="screen_change_server_error_invalid_well_known">"مشہور فائل میں کسی مسئلے کی وجہ سے سلائیڈنگ سنک دستیاب نہیں ہے:
%1$s"</string>
<string name="screen_change_server_form_header">"منزلی خادم عنوان"</string>
<string name="screen_change_server_subtitle">"آپکے خادم کا پتہ کیا ہے؟"</string>
<string name="screen_change_server_title">"اپنا خادم منتخب کریں"</string>
<string name="screen_create_account_title">"کھاتہ تخلیق کریں"</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">"یہ صالح صارف شناسه نہیں ہے۔ متوقع شکل: @صارف:منزلی خادم"</string>
<string name="screen_login_error_refresh_tokens">"یہ خادم تازگی کی رموزِ ممیز استعمال کرنے کے لئے تشکیل دیا گیا ہے۔ لفظ عبور پر مبنی دخول استعمال کرتے ہوئے ان کی حمایت نہیں کی جاتی۔"</string>
<string name="screen_login_error_unsupported_authentication">"منتخب منزلی خادم کلمۂ عبوری یا OIDC دخول کا تعاون نہیں کرتا۔ برائے مہربانی اپنے منتظم سے رابطہ کریں یا کوئی اور منزلی خادم چنیں۔"</string>
<string name="screen_login_form_header">"اپنی تفصیلات درج کریں"</string>
<string name="screen_login_subtitle">"میٹرکس محفوظ، غیر مرکزی مواصلت کے لئے ایک کھلا شبکہ ہے۔"</string>
<string name="screen_login_title">"واپس خوش آمدید!"</string>
<string name="screen_login_title_with_homeserver">"%1$s میں داخل ہوں"</string>
<string name="screen_onboarding_sign_in_manually">"دستی طور پر داخل ہوں"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"کیو آر (QR) رمز کیساتھ داخل ہوں"</string>
<string name="screen_onboarding_sign_up">"کھاتہ تخلیق کریں"</string>
<string name="screen_onboarding_welcome_message">"اب تک کی تیز ترین %1$s میں خوش آمدید۔ رفتار اور سادگی کے لئے مشحون"</string>
<string name="screen_onboarding_welcome_subtitle">"%1$s پر خوش آمدید۔ شحن فائق شدہ، رفتار اور سادگی کیلئے۔"</string>
<string name="screen_onboarding_welcome_title">"اپنے عنصر میں رہیں"</string>
<string name="screen_qr_code_login_connecting_subtitle">"محفوظ اتصال قائم کر رہا ہے"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"نئے آلے سے محفوظ اتصال نہیں بنایا جا سکا۔ آپ کے موجودہ آلات اب بھی محفوظ ہیں اور آپ کو ان کے بارے میں فکر کرنے کی ضرورت نہیں ہے۔"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"اب کیا؟"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"اگر یہ شبکہ کا مسئلہ تھا تو کیو آر رمز کے ساتھ دوبارہ داخل ہونے کی کوشش کریں۔"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"اگر آپ کو بھی یہی مسئلہ درپیش ہو، تو کوئی دوسرا وائی فائی شبکہ آزمائیں یا وائی فائی کے بجائے اپنے محمول بیانات استعمال کریں۔"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"اگر یہ کام نہ کرے، تو دستی طور پر داخل ہوں"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"اتصال محفوظ نہیں"</string>
<string name="screen_qr_code_login_device_code_subtitle">"آپ سے اس آلے پر دکھائے گئے دو ہندسوں کو درج کرنے کو کہا جائے گا۔"</string>
<string name="screen_qr_code_login_device_code_title">"اپنے دوسرے آلے پر درج ذیل نمبر درج کریں"</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"اپنے دوسرے آلے میں داخل ہوں اور پھر دوبارہ کوشش کریں، یا کوئی دوسرا آلہ استعمال کریں جو پہلے سے دخول شدہ ہے۔"</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"دوسرا آلہ دخول شدہ نہیں"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"دوسرے آلے پر دخول منسوخ کر دیا گیا تھا۔"</string>
<string name="screen_qr_code_login_error_cancelled_title">"دخول کی درخواست منسوخ"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"دوسرے آلہ پر دخول کو مسترد کر دیا گیا تھا۔"</string>
<string name="screen_qr_code_login_error_declined_title">"دخول مسترد کیا گیا"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"دخول کی میعاد ختم۔ برائے مہربانی دوبارہ کوشش کریں۔"</string>
<string name="screen_qr_code_login_error_expired_title">"دخول وقت پر مکمل نہیں ہوا تھا"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"آپ کا دوسرا آلہ کیو آر رمز کے ساتھ %s میں دخول کا تعاون نہیں کرتا۔
دستی طور پر داخل ہونے کی کوشش کریں ، یا کسی دوسرے آلے سے کیو آر رمز مسح ضوئی کریں۔"</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"کر رمز غیر تعاون یافتہ"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"آپ کا کھاتہ فراہم کنندہ %1$s کا تعاون نہیں کرتا۔"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s تعاون یافتہ نہیں"</string>
<string name="screen_qr_code_login_initial_state_button_title">"مسح ضوئی کیلئے تیار"</string>
<string name="screen_qr_code_login_initial_state_item_1">"برمیز آلے پر %1$s کھولیں"</string>
<string name="screen_qr_code_login_initial_state_item_2">"اپنے اوتار پر دبائیں"</string>
<string name="screen_qr_code_login_initial_state_item_3">"%1$s منتخب کریں"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"”نیا آلہ مربوط کریں“"</string>
<string name="screen_qr_code_login_initial_state_item_4">"اس آلے کے ساتھ کیو آر رمز مسح ضوئی کریں"</string>
<string name="screen_qr_code_login_initial_state_title">"کیو آر رمز حاصل کرنے کے لئے کسی دوسرے آلے پر %1$s کھولیں"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"دوسرے آلے پر دکھایا گیا کیو آر رمز استعمال کریں۔"</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"دوبارہ کوشش کریں"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"غلط کیو آر رمز"</string>
<string name="screen_qr_code_login_no_camera_permission_button">"تصویرگر کی ترتیبات پر جائیں"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"جاری رکھنے کے لیے آپ %1$s کو اپنے آلے کا تصویرگر استعمال کرنے کی اجازت دینے کی ضرورت ہے۔"</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"کیو آر رمز کو مسح ضوئی کرنے کے لئے تصویرگر تک رسائی کی اجازت دیں"</string>
<string name="screen_qr_code_login_scanning_state_title">"کیو آر رمز مسح ضوئی کریں"</string>
<string name="screen_qr_code_login_start_over_button">"از سر نو شروع کریں"</string>
<string name="screen_qr_code_login_unknown_error_description">"ایک غیر متوقع نقص واقع ہوا۔ برائے مہربانی دوبارہ کوشش کریں۔"</string>
<string name="screen_qr_code_login_verify_code_loading">"آپکے دوسرے آلے کا منتظر"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"آپ کا کھاتہ فراہم کنندہ دخول کی توثیق کے لیے درج ذیل رمز کا مطالبہ کر سکتا ہے۔"</string>
<string name="screen_qr_code_login_verify_code_title">"آپکا توثیقی رمز"</string>
<string name="screen_server_confirmation_change_server">"اکاؤنٹ فراہم کنندہ بدلیں"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"ایلیمنٹ کے ملازمین کیلئے ایک نجی خادم۔"</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"میٹرکس محفوظ، غیر مرکزی مواصلت کے لئے ایک کھلا شبکہ ہے۔"</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>
</resources>

View File

@@ -14,9 +14,11 @@
<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_invalid_well_known">"因為 well-known 檔案的問題,無法使用 sliding sync
<string name="screen_change_server_error_invalid_well_known">"因為 well-known 檔案的問題,伺服器不可用
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"選定的帳號提供者不支援 sliding sync。必須升級伺服器才能使用 %1$s。"</string>
<string name="screen_change_server_form_header">"家伺服器 URL"</string>
<string name="screen_change_server_form_notice">"輸入網域位址。"</string>
<string name="screen_change_server_subtitle">"您的伺服器地址?"</string>
<string name="screen_change_server_title">"選擇您的伺服器"</string>
<string name="screen_create_account_title">"建立帳號"</string>

View File

@@ -89,5 +89,6 @@ Try signing in manually, or scan the QR code with another device."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix is an open network for secure, decentralised communication."</string>
<string name="screen_server_confirmation_message_register">"This is where your conversations will live — just like you would use an email provider to keep your emails."</string>
<string name="screen_server_confirmation_title_login">"Youre about to sign in to %1$s"</string>
<string name="screen_server_confirmation_title_picker_mode">"Choose account provider"</string>
<string name="screen_server_confirmation_title_register">"Youre about to create an account on %1$s"</string>
</resources>

View File

@@ -18,7 +18,7 @@ class DefaultLoginIntentResolverTest {
@Test
fun `nominal case`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/element?account_provider=example.org&login_hint=mxid:@alice:example.org"
val uriString = "https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org"
assertThat(sut.parse(uriString)).isEqualTo(
LoginParams(
accountProvider = "example.org",
@@ -30,7 +30,7 @@ class DefaultLoginIntentResolverTest {
@Test
fun `extra unknown param`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/element?account_provider=example.org&login_hint=mxid:@alice:example.org&extra=uknown"
val uriString = "https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org&extra=uknown"
assertThat(sut.parse(uriString)).isEqualTo(
LoginParams(
accountProvider = "example.org",
@@ -42,7 +42,7 @@ class DefaultLoginIntentResolverTest {
@Test
fun `no account provider`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/element?login_hint=mxid:@alice:example.org"
val uriString = "https://mobile.element.io/element/?login_hint=mxid:@alice:example.org"
assertThat(sut.parse(uriString)).isNull()
}
@@ -63,14 +63,14 @@ class DefaultLoginIntentResolverTest {
@Test
fun `wrong host`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://wrong.element.io/element?account_provider=example.org&login_hint=mxid:@alice:example.org"
val uriString = "https://wrong.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org"
assertThat(sut.parse(uriString)).isNull()
}
@Test
fun `no login_hint param`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/element?account_provider=example.org"
val uriString = "https://mobile.element.io/element/?account_provider=example.org"
assertThat(sut.parse(uriString)).isEqualTo(
LoginParams(
accountProvider = "example.org",

View File

@@ -10,6 +10,7 @@ package io.element.android.features.login.impl.accountprovider
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
@@ -60,6 +61,28 @@ class AccountProviderDataSourceTest {
}
}
@Test
fun `present - ensure that default homeserver is not star char`() = runTest {
val sut = AccountProviderDataSource(
FakeEnterpriseService(
defaultHomeserverListResult = { listOf(EnterpriseService.ANY_ACCOUNT_PROVIDER, AuthenticationConfig.MATRIX_ORG_URL) }
)
)
sut.flow.test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(
AccountProvider(
url = AuthenticationConfig.MATRIX_ORG_URL,
title = "matrix.org",
subtitle = null,
isPublic = true,
isMatrixOrg = true,
isValid = false,
)
)
}
}
@Test
fun `present - user change and reset`() = runTest {
val sut = AccountProviderDataSource(FakeEnterpriseService())

View File

@@ -11,9 +11,12 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.aChangeServerState
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@@ -27,7 +30,9 @@ class ChangeAccountProviderPresenterTest {
fun `present - initial state`() = runTest {
val presenter = ChangeAccountProviderPresenter(
changeServerPresenter = { aChangeServerState() },
enterpriseService = FakeEnterpriseService(),
enterpriseService = FakeEnterpriseService(
defaultHomeserverListResult = { emptyList() }
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -45,6 +50,75 @@ class ChangeAccountProviderPresenterTest {
)
)
)
assertThat(initialState.canSearchForAccountProviders).isTrue()
}
}
@Test
fun `present - fixed list of account providers`() = runTest {
val presenter = ChangeAccountProviderPresenter(
changeServerPresenter = { aChangeServerState() },
enterpriseService = FakeEnterpriseService(
defaultHomeserverListResult = {
listOf(AN_ACCOUNT_PROVIDER, AN_ACCOUNT_PROVIDER_2)
}
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.accountProviders).isEqualTo(
listOf(
AccountProvider(
url = "https://matrix.org",
title = "matrix.org",
subtitle = null,
isPublic = true,
isMatrixOrg = true,
isValid = true,
),
AccountProvider(
url = "https://element.io",
title = "element.io",
subtitle = null,
isPublic = false,
isMatrixOrg = false,
isValid = true,
)
)
)
assertThat(initialState.canSearchForAccountProviders).isFalse()
}
}
@Test
fun `present - opened list of account providers`() = runTest {
val presenter = ChangeAccountProviderPresenter(
changeServerPresenter = { aChangeServerState() },
enterpriseService = FakeEnterpriseService(
defaultHomeserverListResult = {
listOf(AN_ACCOUNT_PROVIDER, EnterpriseService.ANY_ACCOUNT_PROVIDER)
}
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.accountProviders).isEqualTo(
listOf(
AccountProvider(
url = "https://matrix.org",
title = "matrix.org",
subtitle = null,
isPublic = true,
isMatrixOrg = true,
isValid = true,
)
)
)
assertThat(initialState.canSearchForAccountProviders).isTrue()
}
}
}

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