mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Merge branch 'develop' into feature/valere/timeline_active_call_rendering
This commit is contained in:
@@ -69,18 +69,18 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
Trigger the defect, then click on the menu from the room list then "Report a bug". Describe the issue, mentioning that you have also filed a bug (it's helpful if you can include a link to the bug). Send the report to submit anonymous logs to the developers.
|
Trigger the defect, then click on the menu from the room list then "Report a bug". Describe the issue, mentioning that you have also filed a bug (it's helpful if you can include a link to the bug). Send the report to submit anonymous logs to the developers.
|
||||||
options:
|
options:
|
||||||
- 'Yes'
|
|
||||||
- 'No'
|
- 'No'
|
||||||
|
- 'Yes'
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: pr
|
id: pr
|
||||||
attributes:
|
attributes:
|
||||||
label: Are you willing to provide a PR?
|
label: Will you submit a Pull Request?
|
||||||
description: |
|
description: |
|
||||||
Providing a PR can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :).
|
Submitting a Pull Request can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :).
|
||||||
options:
|
options:
|
||||||
- 'Yes'
|
|
||||||
- 'No'
|
- 'No'
|
||||||
|
- 'Yes'
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
name: Enhancement request
|
name: Enhancement request
|
||||||
description: Do you have a suggestion or feature request?
|
description: Do you have a suggestion or feature request? Please open issue on the repo element-hq/element-meta if the request is global to all Element clients.
|
||||||
labels: [T-Enhancement]
|
labels: [T-Enhancement]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion here](https://github.com/element-hq/element-meta/discussions/new?category=ideas).
|
Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please open an issue on the [element-meta repository](https://github.com/element-hq/element-meta/issues/new?template=enhancement.yml) instead.
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: usecase
|
id: usecase
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -104,7 +104,18 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Compile release sources
|
- name: Compile release sources
|
||||||
if: ${{ matrix.variant == 'release' }}
|
if: ${{ matrix.variant == 'release' }}
|
||||||
run: ./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
run: |
|
||||||
|
./gradlew assembleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
- name: Run aapt dump
|
||||||
|
if: ${{ matrix.variant == 'release' }}
|
||||||
|
run: |
|
||||||
|
$ANDROID_HOME/build-tools/37.0.0/aapt dump badging \
|
||||||
|
./app/build/outputs/apk/gplay/release/app-gplay-universal-release.apk \
|
||||||
|
> /tmp/currentAaptDump.txt
|
||||||
|
- name: Check manifest has not changed
|
||||||
|
if: ${{ matrix.variant == 'release' }}
|
||||||
|
run: diff /tmp/currentAaptDump.txt ./tools/manifest/gplay/release/aaptDump.txt
|
||||||
- name: Compile nightly sources
|
- name: Compile nightly sources
|
||||||
if: ${{ matrix.variant == 'nightly' }}
|
if: ${{ matrix.variant == 'nightly' }}
|
||||||
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ jobs:
|
|||||||
./tools/localazy/downloadStrings.sh --all
|
./tools/localazy/downloadStrings.sh --all
|
||||||
./tools/localazy/importSupportedLocalesFromLocalazy.py
|
./tools/localazy/importSupportedLocalesFromLocalazy.py
|
||||||
./tools/test/generateAllScreenshots.py
|
./tools/test/generateAllScreenshots.py
|
||||||
|
./tools/manifest/aaptDump.sh
|
||||||
- name: Create Pull Request for Strings
|
- name: Create Pull Request for Strings
|
||||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -8,9 +8,6 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<!-- To be able to install APK from the application -->
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".ElementXApplication"
|
android:name=".ElementXApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ dependencies {
|
|||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.oauth.api)
|
implementation(projects.libraries.oauth.api)
|
||||||
|
implementation(projects.libraries.permissions.api)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.push.api)
|
implementation(projects.libraries.push.api)
|
||||||
implementation(projects.libraries.pushproviders.api)
|
implementation(projects.libraries.pushproviders.api)
|
||||||
@@ -61,6 +62,7 @@ dependencies {
|
|||||||
testImplementation(projects.features.share.test)
|
testImplementation(projects.features.share.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.oauth.test)
|
testImplementation(projects.libraries.oauth.test)
|
||||||
|
testImplementation(projects.libraries.permissions.test)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ sealed interface LoggedInEvents {
|
|||||||
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
|
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
|
||||||
data object CheckSlidingSyncProxyAvailability : LoggedInEvents
|
data object CheckSlidingSyncProxyAvailability : LoggedInEvents
|
||||||
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
|
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
|
||||||
|
data object DismissLocalNetworkPermissionPrompt : LoggedInEvents
|
||||||
|
|
||||||
|
data object RequestLocationNetworkPermission : LoggedInEvents
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
package io.element.android.appnav.loggedin
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
@@ -15,6 +16,7 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -36,6 +38,10 @@ import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
|
|||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsEvent
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
@@ -58,7 +64,12 @@ class LoggedInPresenter(
|
|||||||
private val encryptionService: EncryptionService,
|
private val encryptionService: EncryptionService,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
|
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
|
||||||
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
) : Presenter<LoggedInState> {
|
) : Presenter<LoggedInState> {
|
||||||
|
private val localNetworkPermissionsPresenter: PermissionsPresenter =
|
||||||
|
permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): LoggedInState {
|
override fun present(): LoggedInState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
@@ -117,6 +128,21 @@ class LoggedInPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val localNetworkPermissionState = localNetworkPermissionsPresenter.present()
|
||||||
|
var localNetworkPromptDismissedThisSession by remember { mutableStateOf(false) }
|
||||||
|
val advisorRequestsPrompt by produceState(initialValue = false, localNetworkPermissionState.permissionGranted) {
|
||||||
|
value = localNetworkPermissionAdvisor.shouldRequestPermissionFor(matrixClient.homeserverUrl)
|
||||||
|
}
|
||||||
|
LaunchedEffect(localNetworkPermissionState.permissionGranted) {
|
||||||
|
if (localNetworkPermissionState.permissionGranted) localNetworkPromptDismissedThisSession = false
|
||||||
|
}
|
||||||
|
val localNetworkPermissionDialog = when {
|
||||||
|
!advisorRequestsPrompt -> LocalNetworkPermissionDialog.None
|
||||||
|
localNetworkPromptDismissedThisSession -> LocalNetworkPermissionDialog.None
|
||||||
|
localNetworkPermissionState.shouldShowRationale -> LocalNetworkPermissionDialog.Rationale
|
||||||
|
else -> LocalNetworkPermissionDialog.Settings
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvent(event: LoggedInEvents) {
|
fun handleEvent(event: LoggedInEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is LoggedInEvents.CloseErrorDialog -> {
|
is LoggedInEvents.CloseErrorDialog -> {
|
||||||
@@ -134,6 +160,17 @@ class LoggedInPresenter(
|
|||||||
// Force the logout since Native Sliding Sync is already enforced by the SDK
|
// Force the logout since Native Sliding Sync is already enforced by the SDK
|
||||||
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
|
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
|
||||||
}
|
}
|
||||||
|
LoggedInEvents.DismissLocalNetworkPermissionPrompt -> {
|
||||||
|
localNetworkPromptDismissedThisSession = true
|
||||||
|
}
|
||||||
|
LoggedInEvents.RequestLocationNetworkPermission -> {
|
||||||
|
if (localNetworkPermissionDialog == LocalNetworkPermissionDialog.Settings) {
|
||||||
|
localNetworkPermissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
|
||||||
|
} else {
|
||||||
|
localNetworkPermissionState.eventSink(PermissionsEvent.RequestPermissions)
|
||||||
|
}
|
||||||
|
localNetworkPromptDismissedThisSession = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +180,7 @@ class LoggedInPresenter(
|
|||||||
ignoreRegistrationError = ignoreRegistrationError,
|
ignoreRegistrationError = ignoreRegistrationError,
|
||||||
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
|
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
|
||||||
appName = buildMeta.applicationName,
|
appName = buildMeta.applicationName,
|
||||||
|
localNetworkPermissionDialog = localNetworkPermissionDialog,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
package io.element.android.appnav.loggedin
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
|
|
||||||
data class LoggedInState(
|
data class LoggedInState(
|
||||||
val showSyncSpinner: Boolean,
|
val showSyncSpinner: Boolean,
|
||||||
@@ -16,5 +17,6 @@ data class LoggedInState(
|
|||||||
val ignoreRegistrationError: Boolean,
|
val ignoreRegistrationError: Boolean,
|
||||||
val forceNativeSlidingSyncMigration: Boolean,
|
val forceNativeSlidingSyncMigration: Boolean,
|
||||||
val appName: String,
|
val appName: String,
|
||||||
|
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
|
||||||
val eventSink: (LoggedInEvents) -> Unit,
|
val eventSink: (LoggedInEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ package io.element.android.appnav.loggedin
|
|||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||||
|
|
||||||
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
||||||
@@ -19,6 +20,8 @@ open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
|||||||
aLoggedInState(showSyncSpinner = true),
|
aLoggedInState(showSyncSpinner = true),
|
||||||
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
|
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
|
||||||
aLoggedInState(forceNativeSlidingSyncMigration = true),
|
aLoggedInState(forceNativeSlidingSyncMigration = true),
|
||||||
|
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale),
|
||||||
|
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Settings),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,11 +30,13 @@ fun aLoggedInState(
|
|||||||
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
|
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||||
forceNativeSlidingSyncMigration: Boolean = false,
|
forceNativeSlidingSyncMigration: Boolean = false,
|
||||||
appName: String = "Element X",
|
appName: String = "Element X",
|
||||||
|
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
|
||||||
) = LoggedInState(
|
) = LoggedInState(
|
||||||
showSyncSpinner = showSyncSpinner,
|
showSyncSpinner = showSyncSpinner,
|
||||||
pusherRegistrationState = pusherRegistrationState,
|
pusherRegistrationState = pusherRegistrationState,
|
||||||
ignoreRegistrationError = false,
|
ignoreRegistrationError = false,
|
||||||
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
|
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
|
||||||
appName = appName,
|
appName = appName,
|
||||||
|
localNetworkPermissionDialog = localNetworkPermissionDialog,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
|||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||||
import io.element.android.libraries.matrix.api.exception.isNetworkError
|
import io.element.android.libraries.matrix.api.exception.isNetworkError
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@@ -82,6 +83,12 @@ fun LoggedInView(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocalNetworkPermissionDialogView(
|
||||||
|
dialog = state.localNetworkPermissionDialog,
|
||||||
|
onSubmit = { state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) },
|
||||||
|
onDismiss = { state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Throwable.getReason(): String? {
|
private fun Throwable.getReason(): String? {
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
|||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
|
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||||
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||||
import io.element.android.libraries.push.test.FakePushService
|
import io.element.android.libraries.push.test.FakePushService
|
||||||
@@ -124,6 +126,8 @@ class LoggedInPresenterTest {
|
|||||||
encryptionService = encryptionService,
|
encryptionService = encryptionService,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||||
).test {
|
).test {
|
||||||
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
||||||
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||||
@@ -368,6 +372,8 @@ class LoggedInPresenterTest {
|
|||||||
encryptionService = encryptionService,
|
encryptionService = encryptionService,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
android:name="android.hardware.microphone"
|
android:name="android.hardware.microphone"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|||||||
@@ -3,14 +3,34 @@
|
|||||||
<string name="screen_create_room_action_create_room">"Ruangan baru"</string>
|
<string name="screen_create_room_action_create_room">"Ruangan baru"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Undang orang-orang"</string>
|
<string name="screen_create_room_add_people_title">"Undang orang-orang"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Terjadi kesalahan saat membuat ruangan"</string>
|
<string name="screen_create_room_error_creating_room">"Terjadi kesalahan saat membuat ruangan"</string>
|
||||||
|
<string name="screen_create_room_error_creating_space">"Ruang tersebut tidak dapat dibuat karena kesalahan yang tidak diketahui. Coba lagi nanti."</string>
|
||||||
|
<string name="screen_create_room_name_placeholder">"Tambahkan nama…"</string>
|
||||||
|
<string name="screen_create_room_new_room_title">"Ruangan baru"</string>
|
||||||
|
<string name="screen_create_room_new_space_title">"Ruang baru"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Hanya orang-orang yang diundang dapat mengakses ruangan ini. Semua pesan terenkripsi secara ujung ke ujung."</string>
|
<string name="screen_create_room_private_option_description">"Hanya orang-orang yang diundang dapat mengakses ruangan ini. Semua pesan terenkripsi secara ujung ke ujung."</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"Pribadi"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Siapa pun dapat mencari ruangan ini.
|
<string name="screen_create_room_public_option_description">"Siapa pun dapat mencari ruangan ini.
|
||||||
Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
|
Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
|
||||||
|
<string name="screen_create_room_public_option_short_description">"Siapa pun bisa bergabung."</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"Umum"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Siapa pun dapat meminta untuk bergabung dengan ijin administrator atau moderator"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Siapa pun dapat meminta untuk bergabung dengan ijin administrator atau moderator"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Minta untuk bergabung"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Minta untuk bergabung"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Siapa pun di %1$s dapat bergabung, tetapi semua orang lain harus meminta akses."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Ajukan permohonan untuk bergabung"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_description">"Hanya orang-orang yang diundang dapat mengakses ruangan ini. Semua pesan terenkripsi secara ujung ke ujung."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_private_option_title">"Pribadi"</string>
|
||||||
<string name="screen_create_room_room_access_section_public_option_description">"Siapa pun dapat bergabung dengan ruangan ini"</string>
|
<string name="screen_create_room_room_access_section_public_option_description">"Siapa pun dapat bergabung dengan ruangan ini"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_public_option_title">"Umum"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_restricted_option_description">"Siapa pun di %1$s dapat bergabung."</string>
|
||||||
|
<string name="screen_create_room_room_access_section_restricted_option_title">"Standar"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_title">"Yang memiliki akses"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."</string>
|
<string name="screen_create_room_room_address_section_footer">"Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Alamat ruangan"</string>
|
<string name="screen_create_room_room_address_section_title">"Alamat ruangan"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"Keterlihatan ruangan"</string>
|
<string name="screen_create_room_room_visibility_section_title">"Keterlihatan ruangan"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_description">"(ruang sudah penuh)"</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_option">"Jangan menambahkan sesuatu ke ruang yang sudah ada."</string>
|
||||||
|
<string name="screen_create_room_space_selection_no_space_title">"Tidak ada ruang yang dipilih"</string>
|
||||||
|
<string name="screen_create_room_space_selection_sheet_title">"Tambahkan ke ruang"</string>
|
||||||
<string name="screen_create_room_topic_label">"Topik (opsional)"</string>
|
<string name="screen_create_room_topic_label">"Topik (opsional)"</string>
|
||||||
|
<string name="screen_create_room_topic_placeholder">"Tambahkan deskripsi…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<string name="screen_create_room_room_access_section_public_option_title">"公開"</string>
|
<string name="screen_create_room_room_access_section_public_option_title">"公開"</string>
|
||||||
<string name="screen_create_room_room_access_section_restricted_option_description">"%1$s にいる全員が参加することができます。"</string>
|
<string name="screen_create_room_room_access_section_restricted_option_description">"%1$s にいる全員が参加することができます。"</string>
|
||||||
<string name="screen_create_room_room_access_section_restricted_option_title">"スタンダード"</string>
|
<string name="screen_create_room_room_access_section_restricted_option_title">"スタンダード"</string>
|
||||||
<string name="screen_create_room_room_access_section_title">"参加できるユーザー"</string>
|
<string name="screen_create_room_room_access_section_title">"参加可能なユーザー"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。"</string>
|
<string name="screen_create_room_room_address_section_footer">"公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。"</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"アドレス"</string>
|
<string name="screen_create_room_room_address_section_title">"アドレス"</string>
|
||||||
<string name="screen_create_room_room_visibility_section_title">"ルームの公開度"</string>
|
<string name="screen_create_room_room_visibility_section_title">"ルームの公開度"</string>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Bekreft at du vil deaktivere kontoen din. Denne handlingen kan ikke angres."</string>
|
<string name="screen_deactivate_account_confirmation_dialog_content">"Bekreft at du vil slette kontoen din. Denne handlingen kan ikke angres."</string>
|
||||||
<string name="screen_deactivate_account_delete_all_messages">"Slett alle meldingene mine"</string>
|
<string name="screen_deactivate_account_delete_all_messages">"Slett alle meldingene mine"</string>
|
||||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Advarsel: Fremtidige brukere vil kunne se ufullstendige samtaler."</string>
|
<string name="screen_deactivate_account_delete_all_messages_notice">"Advarsel: Fremtidige brukere vil kunne se ufullstendige samtaler."</string>
|
||||||
<string name="screen_deactivate_account_description">"Deaktivering av kontoen din er %1$s , det vil:"</string>
|
<string name="screen_deactivate_account_description">"Hvis %1$s sletter kontoen din, vil følgende skje:"</string>
|
||||||
<string name="screen_deactivate_account_description_bold_part">"irreversibel"</string>
|
<string name="screen_deactivate_account_description_bold_part">"irreversibel"</string>
|
||||||
<string name="screen_deactivate_account_list_item_1">"%1$s kontoen din (du kan ikke logge på igjen, og ID-en din kan ikke brukes på nytt)."</string>
|
<string name="screen_deactivate_account_list_item_1">"%1$s kontoen din (du kan ikke logge på igjen, og ID-en din kan ikke brukes på nytt)."</string>
|
||||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Deaktiver permanent"</string>
|
<string name="screen_deactivate_account_list_item_1_bold_part">"Deaktiver permanent"</string>
|
||||||
<string name="screen_deactivate_account_list_item_2">"Fjern deg fra alle chatterom."</string>
|
<string name="screen_deactivate_account_list_item_2">"Fjern deg fra alle chatterom."</string>
|
||||||
<string name="screen_deactivate_account_list_item_3">"Slett kontoinformasjonen din fra vår identitetsserver."</string>
|
<string name="screen_deactivate_account_list_item_3">"Slett kontoinformasjonen din fra vår identitetsserver."</string>
|
||||||
<string name="screen_deactivate_account_list_item_4">"Meldingene dine vil fortsatt være synlige for registrerte brukere, men vil ikke være tilgjengelige for nye eller uregistrerte brukere hvis du velger å slette dem."</string>
|
<string name="screen_deactivate_account_list_item_4">"Meldingene dine vil fortsatt være synlige for registrerte brukere, men vil ikke være tilgjengelige for nye eller uregistrerte brukere hvis du velger å slette dem."</string>
|
||||||
<string name="screen_deactivate_account_title">"Deaktiver kontoen"</string>
|
<string name="screen_deactivate_account_title">"Slett kontoen"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Iltimos, hisobingizni o‘chirishni xohlayotganingizni tasdiqlang. Bu amalni qaytarib bo‘lmaydi."</string>
|
<string name="screen_deactivate_account_confirmation_dialog_content">"Iltimos, hisobingizni oʻchirishni xohlayotganingizni tasdiqlang. Bu amalni qaytarib boʻlmaydi."</string>
|
||||||
<string name="screen_deactivate_account_delete_all_messages">"Barcha xabarlarimni o‘chirib tashlang"</string>
|
<string name="screen_deactivate_account_delete_all_messages">"Barcha xabarlarimni o‘chirib tashlang"</string>
|
||||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Ogohlantirish: Kelgusi foydalanuvchilar chala suhbatlarni ko‘rishi mumkin."</string>
|
<string name="screen_deactivate_account_delete_all_messages_notice">"Ogohlantirish: Kelgusi foydalanuvchilar chala suhbatlarni ko‘rishi mumkin."</string>
|
||||||
<string name="screen_deactivate_account_description">"Hisobingiz %1$s faolsizlantirilmoqda, u quyidagilarni bajaradi:"</string>
|
<string name="screen_deactivate_account_description">"Hisobingiz %1$s faolsizlantirilmoqda, u quyidagilarni bajaradi:"</string>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_identity_confirmation_cannot_confirm">"نمیتوانید تأیید کنید؟"</string>
|
<string name="screen_identity_confirmation_cannot_confirm">"نمیتوانید تأیید کنید؟"</string>
|
||||||
<string name="screen_identity_confirmation_create_new_recovery_key">"ایجاد کلید بازیابی جدید"</string>
|
<string name="screen_identity_confirmation_create_new_recovery_key">"ایجاد کلید بازیابی جدید"</string>
|
||||||
<string name="screen_identity_confirmation_subtitle">"تأیید این افزاره برای برپایی پیامرسانی امن."</string>
|
<string name="screen_identity_confirmation_subtitle">"گزینش چگونگی تأیید برای برپایی پیامرسانی امن."</string>
|
||||||
<string name="screen_identity_confirmation_title">"تأیید هویتتان"</string>
|
<string name="screen_identity_confirmation_title">"تأیید هویت دیجیتالیتان"</string>
|
||||||
<string name="screen_identity_confirmation_use_another_device">"استفاده از افزارهای دیگر"</string>
|
<string name="screen_identity_confirmation_use_another_device">"استفاده از افزارهای دیگر"</string>
|
||||||
<string name="screen_identity_confirmation_use_recovery_key">"استفاده از کلید بازیابی"</string>
|
<string name="screen_identity_confirmation_use_recovery_key">"استفاده از کلید بازیابی"</string>
|
||||||
<string name="screen_identity_confirmed_subtitle">"اکنون میتوانید پیامها را به صورت امن فرستاده و بگیرید و هرکسی که با او گپ میزنید نیز میتواند به این افزاره اعتماد کند."</string>
|
<string name="screen_identity_confirmed_subtitle">"اکنون میتوانید پیامها را به صورت امن فرستاده و بگیرید و هرکسی که با او گپ میزنید نیز میتواند به این افزاره اعتماد کند."</string>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_identity_confirmation_cannot_confirm">"Kan du ikke bekrefte?"</string>
|
<string name="screen_identity_confirmation_cannot_confirm">"Kan du ikke bekrefte?"</string>
|
||||||
<string name="screen_identity_confirmation_create_new_recovery_key">"Opprett en ny gjenopprettingsnøkkel"</string>
|
<string name="screen_identity_confirmation_create_new_recovery_key">"Opprett en ny gjenopprettingsnøkkel"</string>
|
||||||
<string name="screen_identity_confirmation_subtitle">"Verifiser denne enheten for å sette opp sikker meldingsutveksling."</string>
|
<string name="screen_identity_confirmation_subtitle">"Velg hvordan du vil verifisere deg for å sette opp sikker meldingsutveksling."</string>
|
||||||
<string name="screen_identity_confirmation_title">"Bekreft din digitale identitet"</string>
|
<string name="screen_identity_confirmation_title">"Bekreft din digitale identitet"</string>
|
||||||
<string name="screen_identity_confirmation_use_another_device">"Bruk en annen enhet"</string>
|
<string name="screen_identity_confirmation_use_another_device">"Bruk en annen enhet"</string>
|
||||||
<string name="screen_identity_confirmation_use_recovery_key">"Bruk gjenopprettingsnøkkel"</string>
|
<string name="screen_identity_confirmation_use_recovery_key">"Bruk gjenopprettingsnøkkel"</string>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
<string name="banner_battery_optimization_content_android">"Nonaktifkan pengoptimalan baterai untuk aplikasi ini, untuk memastikan semua notifikasi diterima."</string>
|
<string name="banner_battery_optimization_content_android">"Nonaktifkan pengoptimalan baterai untuk aplikasi ini, untuk memastikan semua notifikasi diterima."</string>
|
||||||
<string name="banner_battery_optimization_submit_android">"Nonaktifkan optimasi"</string>
|
<string name="banner_battery_optimization_submit_android">"Nonaktifkan optimasi"</string>
|
||||||
<string name="banner_battery_optimization_title_android">"Notifikasi tidak masuk?"</string>
|
<string name="banner_battery_optimization_title_android">"Notifikasi tidak masuk?"</string>
|
||||||
|
<string name="banner_new_sound_message">"Pemberitahuan Anda telah diperbarui—lebih jelas, lebih cepat, dan tidak mengganggu."</string>
|
||||||
|
<string name="banner_new_sound_title">"Kami telah memperbarui suara Anda."</string>
|
||||||
<string name="banner_set_up_recovery_content">"Buat kunci pemulihan baru yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi Anda jika Anda kehilangan akses ke perangkat Anda."</string>
|
<string name="banner_set_up_recovery_content">"Buat kunci pemulihan baru yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi Anda jika Anda kehilangan akses ke perangkat Anda."</string>
|
||||||
<string name="banner_set_up_recovery_submit">"Siapkan Kunci pemulihan"</string>
|
<string name="banner_set_up_recovery_submit">"Siapkan Kunci pemulihan"</string>
|
||||||
<string name="banner_set_up_recovery_title">"Cadangkan obrolan Anda"</string>
|
<string name="banner_set_up_recovery_title">"Cadangkan obrolan Anda"</string>
|
||||||
@@ -48,6 +50,7 @@ Anda tidak memiliki pesan yang belum dibaca!"</string>
|
|||||||
<string name="screen_roomlist_mark_as_read">"Tandai sebagai dibaca"</string>
|
<string name="screen_roomlist_mark_as_read">"Tandai sebagai dibaca"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Tandai sebagai belum dibaca"</string>
|
<string name="screen_roomlist_mark_as_unread">"Tandai sebagai belum dibaca"</string>
|
||||||
<string name="screen_roomlist_tombstoned_room_description">"Ruangan ini telah ditingkatkan"</string>
|
<string name="screen_roomlist_tombstoned_room_description">"Ruangan ini telah ditingkatkan"</string>
|
||||||
|
<string name="screen_roomlist_your_spaces">"Ruang Anda"</string>
|
||||||
<string name="session_verification_banner_message">"Sepertinya Anda menggunakan perangkat baru. Verifikasi dengan perangkat lain untuk mengakses pesan terenkripsi Anda selanjutnya."</string>
|
<string name="session_verification_banner_message">"Sepertinya Anda menggunakan perangkat baru. Verifikasi dengan perangkat lain untuk mengakses pesan terenkripsi Anda selanjutnya."</string>
|
||||||
<string name="session_verification_banner_title">"Verifikasi bahwa ini Anda"</string>
|
<string name="session_verification_banner_title">"Verifikasi bahwa ini Anda"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
<string name="banner_battery_optimization_title_android">"Kommer ikke varslene frem?"</string>
|
<string name="banner_battery_optimization_title_android">"Kommer ikke varslene frem?"</string>
|
||||||
<string name="banner_new_sound_message">"Varslingssignalet ditt er oppdatert – tydeligere, raskere og mindre forstyrrende."</string>
|
<string name="banner_new_sound_message">"Varslingssignalet ditt er oppdatert – tydeligere, raskere og mindre forstyrrende."</string>
|
||||||
<string name="banner_new_sound_title">"Vi har oppdatert lydene dine"</string>
|
<string name="banner_new_sound_title">"Vi har oppdatert lydene dine"</string>
|
||||||
<string name="banner_set_up_recovery_content">"Gjenopprett din kryptografiske identitet og meldingshistorikk med en gjenopprettingsnøkkel hvis du har mistet alle dine brukte enheter."</string>
|
<string name="banner_set_up_recovery_content">"Chattene dine sikkerhetskopieres automatisk med ende-til-ende-kryptering. For å gjenopprette denne sikkerhetskopien og beholde din digitale identitet når du mister tilgang til alle enhetene dine, trenger du gjenopprettingsnøkkelen din."</string>
|
||||||
<string name="banner_set_up_recovery_submit">"Hent gjenopprettingsnøkkel"</string>
|
<string name="banner_set_up_recovery_submit">"Hent gjenopprettingsnøkkel"</string>
|
||||||
<string name="banner_set_up_recovery_title">"Konfigurer gjenoppretting for å beskytte kontoen din"</string>
|
<string name="banner_set_up_recovery_title">"Sikkerhetskopier chattene dine"</string>
|
||||||
<string name="confirm_recovery_key_banner_message">"Verifiser gjenopprettingsnøkkelen for å opprettholde tilgangen til nøkkellageret og meldingshistorikken."</string>
|
<string name="confirm_recovery_key_banner_message">"Verifiser gjenopprettingsnøkkelen for å opprettholde tilgangen til nøkkellageret og meldingshistorikken."</string>
|
||||||
<string name="confirm_recovery_key_banner_primary_button_title">"Skriv inn gjenopprettingsnøkkelen din"</string>
|
<string name="confirm_recovery_key_banner_primary_button_title">"Skriv inn gjenopprettingsnøkkelen din"</string>
|
||||||
<string name="confirm_recovery_key_banner_secondary_button_title">"Har du glemt din gjenopprettingsnøkkel?"</string>
|
<string name="confirm_recovery_key_banner_secondary_button_title">"Har du glemt din gjenopprettingsnøkkel?"</string>
|
||||||
|
|||||||
@@ -2,4 +2,8 @@
|
|||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_invite_users_already_a_member">"Sudah menjadi anggota"</string>
|
<string name="screen_invite_users_already_a_member">"Sudah menjadi anggota"</string>
|
||||||
<string name="screen_invite_users_already_invited">"Sudah diundang"</string>
|
<string name="screen_invite_users_already_invited">"Sudah diundang"</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"Saat ini Anda belum memiliki obrolan dengan kontak-kontak ini. Konfirmasikan undangan untuk mengundang mereka ke ruang obrolan ini sebelum melanjutkan."</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"Saat ini Anda belum memiliki obrolan apa pun dengan kontak ini. Konfirmasikan undangan ke ruang obrolan ini sebelum melanjutkan."</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"Ingin mengundang kontak baru ke ruang ini?"</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_title_one_user">"Ingin mengundang kontak baru ke ruang ini?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -2,4 +2,8 @@
|
|||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_invite_users_already_a_member">"Allaqachon a\'zo"</string>
|
<string name="screen_invite_users_already_a_member">"Allaqachon a\'zo"</string>
|
||||||
<string name="screen_invite_users_already_invited">"Allaqachon taklif qilingan"</string>
|
<string name="screen_invite_users_already_invited">"Allaqachon taklif qilingan"</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"Hozirda ushbu kontaktlar bilan hech qanday chatingiz yoʻq. Davom etishdan oldin ularni ushbu xonaga taklif qilishni tasdiqlang."</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"Hozirda bu kontakt bilan hech qanday chatingiz yoʻq. Davom etishdan oldin ularni ushbu xonaga taklif qilishni tasdiqlang."</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"Bu xonaga yangi kontaktlar taklif qilinsinmi?"</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_title_one_user">"Bu xonaga yangi kontakt taklif qilinsinmi?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
<string name="screen_link_new_device_mobile_step2">"گزینش %1$s"</string>
|
<string name="screen_link_new_device_mobile_step2">"گزینش %1$s"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"نتوانست اتّصالی امن به افزارهٔ جدید بسازد. افزارههای موجودتان هنوز امنند و نیازی نیست نگرانشان باشید."</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_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_list_item_3">"ورود دستی در صورت کار نکردنش"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"اتّصال ناامن"</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_subtitle">"از شما خواسته خواهد شد که دو رقم نشان داده روی این افزاره را وارد کنید."</string>
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_link_new_device_desktop_scanning_title">"Pindai kode QR"</string>
|
<string name="screen_link_new_device_desktop_scanning_title">"Pindai kode QR"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_step1">"Buka %1$s di laptop atau komputer desktop."</string>
|
||||||
<string name="screen_link_new_device_desktop_step3">"Pindai kode QR dengan perangkat ini"</string>
|
<string name="screen_link_new_device_desktop_step3">"Pindai kode QR dengan perangkat ini"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"Siap untuk memindai"</string>
|
<string name="screen_link_new_device_desktop_submit">"Siap untuk memindai"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_title">"Buka %1$s di komputer desktop untuk mendapatkan kode QR."</string>
|
||||||
|
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Angkanya tidak cocok"</string>
|
||||||
|
<string name="screen_link_new_device_enter_number_notice">"Masukkan kode 2 digit"</string>
|
||||||
|
<string name="screen_link_new_device_enter_number_subtitle">"Ini akan memverifikasi bahwa koneksi ke perangkat Anda yang lain aman."</string>
|
||||||
|
<string name="screen_link_new_device_enter_number_title">"Masukkan nomor yang ditampilkan di perangkat Anda yang lain"</string>
|
||||||
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Penyedia akun Anda tidak mendukung %1$s."</string>
|
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Penyedia akun Anda tidak mendukung %1$s."</string>
|
||||||
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s tidak didukung"</string>
|
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s tidak didukung"</string>
|
||||||
|
<string name="screen_link_new_device_error_not_supported_subtitle">"Penyedia akun Anda tidak mendukung masuk ke perangkat baru menggunakan kode QR."</string>
|
||||||
<string name="screen_link_new_device_error_not_supported_title">"Kode QR tidak didukung"</string>
|
<string name="screen_link_new_device_error_not_supported_title">"Kode QR tidak didukung"</string>
|
||||||
<string name="screen_link_new_device_error_request_cancelled_subtitle">"Proses masuk dibatalkan di perangkat lain."</string>
|
<string name="screen_link_new_device_error_request_cancelled_subtitle">"Proses masuk dibatalkan di perangkat lain."</string>
|
||||||
<string name="screen_link_new_device_error_request_cancelled_title">"Permintaan masuk dibatalkan"</string>
|
<string name="screen_link_new_device_error_request_cancelled_title">"Permintaan masuk dibatalkan"</string>
|
||||||
<string name="screen_link_new_device_error_request_timeout_subtitle">"Masa masuk kedaluwarsa. Silakan coba lagi."</string>
|
<string name="screen_link_new_device_error_request_timeout_subtitle">"Masa masuk kedaluwarsa. Silakan coba lagi."</string>
|
||||||
<string name="screen_link_new_device_error_request_timeout_title">"Proses masuk tidak selesai tepat waktu"</string>
|
<string name="screen_link_new_device_error_request_timeout_title">"Proses masuk tidak selesai tepat waktu"</string>
|
||||||
|
<string name="screen_link_new_device_mobile_step1">"Buka %1$s di perangkat lain."</string>
|
||||||
<string name="screen_link_new_device_mobile_step2">"Pilih %1$s"</string>
|
<string name="screen_link_new_device_mobile_step2">"Pilih %1$s"</string>
|
||||||
|
<string name="screen_link_new_device_mobile_step2_action">"Masuk dengan kode QR"</string>
|
||||||
|
<string name="screen_link_new_device_mobile_step3">"Pindai kode QR yang ditampilkan di sini dengan perangkat lain."</string>
|
||||||
|
<string name="screen_link_new_device_mobile_title">"Buka %1$s di perangkat lain."</string>
|
||||||
|
<string name="screen_link_new_device_root_desktop_computer">"Komputer desktop"</string>
|
||||||
|
<string name="screen_link_new_device_root_loading_qr_code">"Memuat kode QR…"</string>
|
||||||
|
<string name="screen_link_new_device_root_mobile_device">"Perangkat seluler"</string>
|
||||||
|
<string name="screen_link_new_device_root_title">"Jenis perangkat apa yang ingin Anda tautkan?"</string>
|
||||||
|
<string name="screen_link_new_device_wrong_number_subtitle">"Silakan coba lagi dan pastikan Anda telah memasukkan kode 2 digit dengan benar. Jika angka-angka tersebut masih tidak cocok, hubungi penyedia akun Anda."</string>
|
||||||
|
<string name="screen_link_new_device_wrong_number_title">"Angkanya tidak cocok"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Koneksi aman tidak dapat dibuat ke perangkat baru. Perangkat Anda yang ada masih aman dan Anda tidak perlu khawatir tentang mereka."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Koneksi aman tidak dapat dibuat ke perangkat baru. Perangkat Anda yang ada masih aman dan Anda tidak perlu khawatir tentang mereka."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Apa sekarang?"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Apa sekarang?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Coba masuk lagi dengan kode QR jika ini adalah masalah jaringan"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Coba masuk lagi dengan kode QR jika ini adalah masalah jaringan"</string>
|
||||||
@@ -23,6 +40,8 @@
|
|||||||
<string name="screen_qr_code_login_error_cancelled_title">"Permintaan masuk dibatalkan"</string>
|
<string name="screen_qr_code_login_error_cancelled_title">"Permintaan masuk dibatalkan"</string>
|
||||||
<string name="screen_qr_code_login_error_declined_subtitle">"Proses masuk ditolak di perangkat lain."</string>
|
<string name="screen_qr_code_login_error_declined_subtitle">"Proses masuk ditolak di perangkat lain."</string>
|
||||||
<string name="screen_qr_code_login_error_declined_title">"Proses masuk ditolak"</string>
|
<string name="screen_qr_code_login_error_declined_title">"Proses masuk ditolak"</string>
|
||||||
|
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Anda tidak perlu melakukan hal lain."</string>
|
||||||
|
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Perangkat Anda yang lain sudah masuk"</string>
|
||||||
<string name="screen_qr_code_login_error_expired_subtitle">"Masa masuk kedaluwarsa. Silakan coba lagi."</string>
|
<string name="screen_qr_code_login_error_expired_subtitle">"Masa masuk kedaluwarsa. Silakan coba lagi."</string>
|
||||||
<string name="screen_qr_code_login_error_expired_title">"Proses masuk tidak selesai tepat waktu"</string>
|
<string name="screen_qr_code_login_error_expired_title">"Proses masuk tidak selesai tepat waktu"</string>
|
||||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Perangkat Anda yang lain tidak mendukung masuk ke %s dengan kode QR.
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Perangkat Anda yang lain tidak mendukung masuk ke %s dengan kode QR.
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_share_location_live_location_disclaimer_title">"Vaša povijest lokacije uživo bit će pohranjena u sobi i vidljiva članovima nakon završetka sesije."</string>
|
<string name="screen_share_location_live_location_disclaimer_title">"Vaša povijest lokacije uživo bit će pohranjena u sobi i vidljiva članovima nakon završetka sesije."</string>
|
||||||
<string name="screen_share_location_live_location_duration_picker_title">"Odaberite koliko dugo želite dijeliti svoju lokaciju uživo."</string>
|
<string name="screen_share_location_live_location_duration_picker_title">"Odaberite koliko dugo želite dijeliti svoju lokaciju uživo."</string>
|
||||||
|
<string name="screen_share_location_live_location_missing_permissions">"Nemate dopuštenja za dijeljenje svoje lokacije uživo u ovoj sobi"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_share_location_live_location_disclaimer_title">"Riwayat lokasi langsung Anda akan disimpan di ruang obrolan dan dapat dilihat oleh anggota setelah sesi berakhir."</string>
|
||||||
|
<string name="screen_share_location_live_location_duration_picker_title">"Pilih berapa lama Anda ingin membagikan lokasi langsung Anda."</string>
|
||||||
|
<string name="screen_share_location_live_location_missing_permissions">"Anda tidak memiliki izin untuk membagikan lokasi real-time Anda di ruang ini"</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_share_location_live_location_duration_picker_title">"Velg hvor lenge du vil dele din sanntidslokasjon."</string>
|
||||||
|
</resources>
|
||||||
@@ -2,4 +2,5 @@
|
|||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_share_location_live_location_disclaimer_title">"Jonli joylashuv tarixingiz chat-xonada saqlanadi va sessiya tugaganidan keyin homiylarga ko‘rinadi."</string>
|
<string name="screen_share_location_live_location_disclaimer_title">"Jonli joylashuv tarixingiz chat-xonada saqlanadi va sessiya tugaganidan keyin homiylarga ko‘rinadi."</string>
|
||||||
<string name="screen_share_location_live_location_duration_picker_title">"Jonli joylashuvingiz qancha vaqt ulashilishini tanlang."</string>
|
<string name="screen_share_location_live_location_duration_picker_title">"Jonli joylashuvingiz qancha vaqt ulashilishini tanlang."</string>
|
||||||
|
<string name="screen_share_location_live_location_missing_permissions">"Bu xonada jonli joylashuvingizni ulashish uchun sizda ruxsat yoʻq"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+49
-18
@@ -15,15 +15,23 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
@@ -32,6 +40,8 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
|||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.pinDigitBg
|
import io.element.android.libraries.designsystem.theme.pinDigitBg
|
||||||
|
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PinEntryTextField(
|
fun PinEntryTextField(
|
||||||
@@ -39,16 +49,25 @@ fun PinEntryTextField(
|
|||||||
isSecured: Boolean,
|
isSecured: Boolean,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
initialIsFocus: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
var isFocused by remember { mutableStateOf(initialIsFocus) }
|
||||||
|
val filledCount = pinEntry.digits.count { it is PinDigit.Filled }
|
||||||
|
val pinFieldLabel = stringResource(CommonStrings.a11y_pin_field)
|
||||||
|
val digitsEnteredLabel = pluralStringResource(CommonPlurals.a11y_digits_entered, filledCount, filledCount)
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
modifier = modifier,
|
modifier = modifier
|
||||||
|
.onFocusChanged { isFocused = it.isFocused }
|
||||||
|
.clearAndSetSemantics {
|
||||||
|
contentDescription = "$pinFieldLabel, $digitsEnteredLabel"
|
||||||
|
},
|
||||||
value = pinEntry.toText(),
|
value = pinEntry.toText(),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
onValueChange(it)
|
onValueChange(it)
|
||||||
},
|
},
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
||||||
decorationBox = {
|
decorationBox = {
|
||||||
PinEntryRow(pinEntry = pinEntry, isSecured = isSecured)
|
PinEntryRow(pinEntry = pinEntry, isSecured = isSecured, isFocused = isFocused)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -58,13 +77,19 @@ fun PinEntryTextField(
|
|||||||
private fun PinEntryRow(
|
private fun PinEntryRow(
|
||||||
pinEntry: PinEntry,
|
pinEntry: PinEntry,
|
||||||
isSecured: Boolean,
|
isSecured: Boolean,
|
||||||
|
isFocused: Boolean,
|
||||||
) {
|
) {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
for (digit in pinEntry.digits) {
|
val focusedIndex = pinEntry.digits.indexOfFirst { it is PinDigit.Empty }
|
||||||
PinDigitView(digit = digit, isSecured = isSecured)
|
pinEntry.digits.forEachIndexed { index, digit ->
|
||||||
|
PinDigitView(
|
||||||
|
digit = digit,
|
||||||
|
isSecured = isSecured,
|
||||||
|
isFocused = isFocused && index == focusedIndex
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,11 +98,16 @@ private fun PinEntryRow(
|
|||||||
private fun PinDigitView(
|
private fun PinDigitView(
|
||||||
digit: PinDigit,
|
digit: PinDigit,
|
||||||
isSecured: Boolean,
|
isSecured: Boolean,
|
||||||
|
isFocused: Boolean,
|
||||||
) {
|
) {
|
||||||
val shape = RoundedCornerShape(8.dp)
|
val shape = RoundedCornerShape(8.dp)
|
||||||
val appearanceModifier = when (digit) {
|
val appearanceModifier = when (digit) {
|
||||||
PinDigit.Empty -> {
|
PinDigit.Empty -> {
|
||||||
Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape)
|
if (isFocused) {
|
||||||
|
Modifier.border(2.dp, ElementTheme.colors.borderFocused, shape)
|
||||||
|
} else {
|
||||||
|
Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is PinDigit.Filled -> {
|
is PinDigit.Filled -> {
|
||||||
Modifier.background(ElementTheme.colors.pinDigitBg, shape)
|
Modifier.background(ElementTheme.colors.pinDigitBg, shape)
|
||||||
@@ -108,18 +138,19 @@ private fun PinDigitView(
|
|||||||
internal fun PinEntryTextFieldPreview() {
|
internal fun PinEntryTextFieldPreview() {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
val pinEntry = PinEntry.createEmpty(4).fillWith("12")
|
val pinEntry = PinEntry.createEmpty(4).fillWith("12")
|
||||||
Column {
|
Column(
|
||||||
PinEntryTextField(
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
pinEntry = pinEntry,
|
) {
|
||||||
isSecured = true,
|
listOf(true, false).forEach { isSecured ->
|
||||||
onValueChange = {},
|
listOf(true, false).forEach { initialIsFocus ->
|
||||||
)
|
PinEntryTextField(
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
pinEntry = pinEntry,
|
||||||
PinEntryTextField(
|
isSecured = isSecured,
|
||||||
pinEntry = pinEntry,
|
initialIsFocus = initialIsFocus,
|
||||||
isSecured = false,
|
onValueChange = {},
|
||||||
onValueChange = {},
|
)
|
||||||
)
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -13,4 +13,6 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
|
|||||||
sealed interface ChangeServerEvents {
|
sealed interface ChangeServerEvents {
|
||||||
data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents
|
data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents
|
||||||
data object ClearError : ChangeServerEvents
|
data object ClearError : ChangeServerEvents
|
||||||
|
data object RequestLocalNetworkPermission : ChangeServerEvents
|
||||||
|
data object DismissLocalNetworkPermission : ChangeServerEvents
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-8
@@ -12,50 +12,55 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class ChangeServerPresenter(
|
class ChangeServerPresenter(
|
||||||
private val authenticationService: MatrixAuthenticationService,
|
private val authenticationService: MatrixAuthenticationService,
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl,
|
private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl,
|
||||||
|
private val localNetworkPermissionGate: LocalNetworkPermissionGate,
|
||||||
) : Presenter<ChangeServerState> {
|
) : Presenter<ChangeServerState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ChangeServerState {
|
override fun present(): ChangeServerState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val changeServerAction: MutableState<AsyncData<Unit>> = remember {
|
val changeServerAction: MutableState<AsyncData<Unit>> = remember {
|
||||||
mutableStateOf(AsyncData.Uninitialized)
|
mutableStateOf(AsyncData.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val gateState = localNetworkPermissionGate.present<AccountProvider>(
|
||||||
|
urlOf = { provider -> provider.url },
|
||||||
|
onProceed = { provider -> changeServer(provider, changeServerAction) },
|
||||||
|
)
|
||||||
|
|
||||||
fun handleEvent(event: ChangeServerEvents) {
|
fun handleEvent(event: ChangeServerEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction)
|
|
||||||
ChangeServerEvents.ClearError -> changeServerAction.value = AsyncData.Uninitialized
|
ChangeServerEvents.ClearError -> changeServerAction.value = AsyncData.Uninitialized
|
||||||
|
is ChangeServerEvents.ChangeServer -> gateState.submit(event.accountProvider)
|
||||||
|
ChangeServerEvents.DismissLocalNetworkPermission -> gateState.abort()
|
||||||
|
ChangeServerEvents.RequestLocalNetworkPermission -> gateState.requestPermission()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ChangeServerState(
|
return ChangeServerState(
|
||||||
changeServerAction = changeServerAction.value,
|
changeServerAction = changeServerAction.value,
|
||||||
|
localNetworkPermissionDialog = gateState.dialog,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.changeServer(
|
private suspend fun changeServer(
|
||||||
data: AccountProvider,
|
data: AccountProvider,
|
||||||
changeServerAction: MutableState<AsyncData<Unit>>,
|
changeServerAction: MutableState<AsyncData<Unit>>,
|
||||||
) = launch {
|
) {
|
||||||
suspend {
|
suspend {
|
||||||
defaultAccountProviderAccessControl.assertIsAllowedToConnectToAccountProvider(
|
defaultAccountProviderAccessControl.assertIsAllowedToConnectToAccountProvider(
|
||||||
title = data.title,
|
title = data.title,
|
||||||
|
|||||||
+2
@@ -9,8 +9,10 @@
|
|||||||
package io.element.android.features.login.impl.changeserver
|
package io.element.android.features.login.impl.changeserver
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
|
|
||||||
data class ChangeServerState(
|
data class ChangeServerState(
|
||||||
val changeServerAction: AsyncData<Unit>,
|
val changeServerAction: AsyncData<Unit>,
|
||||||
|
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
|
||||||
val eventSink: (ChangeServerEvents) -> Unit
|
val eventSink: (ChangeServerEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|||||||
+4
@@ -11,6 +11,7 @@ package io.element.android.features.login.impl.changeserver
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
|
|
||||||
open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> {
|
open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> {
|
||||||
override val values: Sequence<ChangeServerState>
|
override val values: Sequence<ChangeServerState>
|
||||||
@@ -39,12 +40,15 @@ open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerStat
|
|||||||
ChangeServerError.UnsupportedServer
|
ChangeServerError.UnsupportedServer
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
aChangeServerState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aChangeServerState(
|
fun aChangeServerState(
|
||||||
changeServerAction: AsyncData<Unit> = AsyncData.Uninitialized,
|
changeServerAction: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||||
|
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
|
||||||
) = ChangeServerState(
|
) = ChangeServerState(
|
||||||
changeServerAction = changeServerAction,
|
changeServerAction = changeServerAction,
|
||||||
|
localNetworkPermissionDialog = localNetworkPermissionDialog,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|||||||
+10
@@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
|||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -121,6 +122,15 @@ fun ChangeServerView(
|
|||||||
}
|
}
|
||||||
AsyncData.Uninitialized -> Unit
|
AsyncData.Uninitialized -> Unit
|
||||||
}
|
}
|
||||||
|
LocalNetworkPermissionDialogView(
|
||||||
|
dialog = state.localNetworkPermissionDialog,
|
||||||
|
onSubmit = {
|
||||||
|
eventSink.invoke(ChangeServerEvents.RequestLocalNetworkPermission)
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
eventSink.invoke(ChangeServerEvents.DismissLocalNetworkPermission)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
|
|||||||
+5
@@ -14,6 +14,8 @@ import dev.zacsweers.metro.Binds
|
|||||||
import dev.zacsweers.metro.ContributesTo
|
import dev.zacsweers.metro.ContributesTo
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
|
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||||
|
import io.element.android.features.login.impl.login.LoginModePresenter
|
||||||
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(AppScope::class)
|
||||||
@@ -21,4 +23,7 @@ import io.element.android.libraries.architecture.Presenter
|
|||||||
interface LoginModule {
|
interface LoginModule {
|
||||||
@Binds
|
@Binds
|
||||||
fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter<ChangeServerState>
|
fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter<ChangeServerState>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindLoginModePresenter(presenter: LoginModePresenter): Presenter<LoginModeState>
|
||||||
}
|
}
|
||||||
|
|||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.localnetwork
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsEvent
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
class LocalNetworkPermissionGate(
|
||||||
|
private val advisor: LocalNetworkPermissionAdvisor,
|
||||||
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
|
) {
|
||||||
|
private val permissionsPresenter: PermissionsPresenter =
|
||||||
|
permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T : Any> present(
|
||||||
|
urlOf: (T) -> String,
|
||||||
|
onProceed: suspend (T) -> Unit,
|
||||||
|
): LocalNetworkPermissionGateState<T> {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val permissionsState = permissionsPresenter.present()
|
||||||
|
var pendingSubmit by remember { mutableStateOf<T?>(null) }
|
||||||
|
|
||||||
|
val latestUrlOf by rememberUpdatedState(urlOf)
|
||||||
|
val latestOnProceed by rememberUpdatedState(onProceed)
|
||||||
|
|
||||||
|
LaunchedEffect(permissionsState.permissionGranted, pendingSubmit) {
|
||||||
|
val pending = pendingSubmit
|
||||||
|
if (pending != null && permissionsState.permissionGranted) {
|
||||||
|
coroutineScope.launch { latestOnProceed(pending) }
|
||||||
|
pendingSubmit = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog by rememberUpdatedState(
|
||||||
|
when {
|
||||||
|
pendingSubmit == null -> LocalNetworkPermissionDialog.None
|
||||||
|
permissionsState.permissionGranted -> LocalNetworkPermissionDialog.None
|
||||||
|
permissionsState.shouldShowRationale || !permissionsState.permissionAlreadyAsked -> LocalNetworkPermissionDialog.Rationale
|
||||||
|
else -> LocalNetworkPermissionDialog.Settings
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
fun submit(value: T) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (advisor.shouldRequestPermissionFor(latestUrlOf(value))) {
|
||||||
|
pendingSubmit = value
|
||||||
|
} else {
|
||||||
|
latestOnProceed(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestPermission() {
|
||||||
|
when (dialog) {
|
||||||
|
LocalNetworkPermissionDialog.Settings -> {
|
||||||
|
permissionsState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
|
||||||
|
}
|
||||||
|
LocalNetworkPermissionDialog.Rationale -> {
|
||||||
|
permissionsState.eventSink(PermissionsEvent.ForceRequestPermissions)
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun abort() {
|
||||||
|
pendingSubmit = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return LocalNetworkPermissionGateState(
|
||||||
|
dialog = dialog,
|
||||||
|
submit = ::submit,
|
||||||
|
requestPermission = ::requestPermission,
|
||||||
|
abort = ::abort,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.localnetwork
|
||||||
|
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
|
|
||||||
|
data class LocalNetworkPermissionGateState<T>(
|
||||||
|
val dialog: LocalNetworkPermissionDialog,
|
||||||
|
val submit: (T) -> Unit,
|
||||||
|
val requestPermission: () -> Unit,
|
||||||
|
val abort: () -> Unit,
|
||||||
|
)
|
||||||
-128
@@ -1,128 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* 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.login
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import dev.zacsweers.metro.Inject
|
|
||||||
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
|
|
||||||
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
|
||||||
import io.element.android.libraries.matrix.api.auth.OAuthPrompt
|
|
||||||
import io.element.android.libraries.oauth.api.OAuthAction
|
|
||||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is responsible for managing the login flow, including handling OIDC actions and
|
|
||||||
* submitting login requests.
|
|
||||||
* It's a helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter]
|
|
||||||
* and [ChooseAccountProviderPresenter].
|
|
||||||
*/
|
|
||||||
@Inject
|
|
||||||
class LoginHelper(
|
|
||||||
private val oAuthActionFlow: OAuthActionFlow,
|
|
||||||
private val authenticationService: MatrixAuthenticationService,
|
|
||||||
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
|
|
||||||
) {
|
|
||||||
private val loginModeState: MutableState<AsyncData<LoginMode>> = mutableStateOf(AsyncData.Uninitialized)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun collectLoginMode(): State<AsyncData<LoginMode>> {
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
oAuthActionFlow.collect { oAuthAction ->
|
|
||||||
if (oAuthAction != null) {
|
|
||||||
onOAuthAction(oAuthAction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return loginModeState
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearError() {
|
|
||||||
loginModeState.value = AsyncData.Uninitialized
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun submit(
|
|
||||||
isAccountCreation: Boolean,
|
|
||||||
homeserverUrl: String,
|
|
||||||
resolvedHomeserverUrl: String?,
|
|
||||||
loginHint: String?,
|
|
||||||
) {
|
|
||||||
suspend {
|
|
||||||
authenticationService.setHomeserver(homeserverUrl).recoverCatching {
|
|
||||||
// No .well-known file?
|
|
||||||
// If the homeserver is not reachable, try using resolvedHomeserverUrl.
|
|
||||||
if (resolvedHomeserverUrl != null && resolvedHomeserverUrl != homeserverUrl) {
|
|
||||||
authenticationService.setHomeserver(resolvedHomeserverUrl).getOrThrow()
|
|
||||||
} else {
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}.map { matrixHomeServerDetails ->
|
|
||||||
if (matrixHomeServerDetails.supportsOAuthLogin) {
|
|
||||||
// Retrieve the details right now
|
|
||||||
val oAuthPrompt = if (isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login
|
|
||||||
LoginMode.OAuth(
|
|
||||||
authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = loginHint).getOrThrow()
|
|
||||||
)
|
|
||||||
} else if (isAccountCreation) {
|
|
||||||
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)
|
|
||||||
LoginMode.AccountCreation(url)
|
|
||||||
} else if (matrixHomeServerDetails.supportsPasswordLogin) {
|
|
||||||
LoginMode.PasswordLogin
|
|
||||||
} else {
|
|
||||||
error("Unsupported login flow")
|
|
||||||
}
|
|
||||||
}.getOrThrow()
|
|
||||||
}.runCatchingUpdatingState(
|
|
||||||
state = loginModeState,
|
|
||||||
errorTransform = {
|
|
||||||
when (it) {
|
|
||||||
is AccountCreationNotSupported -> it
|
|
||||||
else -> ChangeServerError.from(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun onOAuthAction(oAuthAction: OAuthAction) {
|
|
||||||
if (oAuthAction is OAuthAction.GoBack && oAuthAction.toUnblock && loginModeState.value !is AsyncData.Loading) {
|
|
||||||
// Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode.
|
|
||||||
// This can happen if there is an error, for instance attempt to login again on the same account.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
loginModeState.value = AsyncData.Loading()
|
|
||||||
when (oAuthAction) {
|
|
||||||
is OAuthAction.GoBack -> {
|
|
||||||
authenticationService.cancelOAuthLogin()
|
|
||||||
.onSuccess {
|
|
||||||
loginModeState.value = AsyncData.Uninitialized
|
|
||||||
}
|
|
||||||
.onFailure { failure ->
|
|
||||||
loginModeState.value = AsyncData.Failure(failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is OAuthAction.Success -> {
|
|
||||||
authenticationService.loginWithOAuth(oAuthAction.url)
|
|
||||||
.onFailure { failure ->
|
|
||||||
loginModeState.value = AsyncData.Failure(failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
oAuthActionFlow.reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.login
|
||||||
|
|
||||||
|
sealed interface LoginModeEvent {
|
||||||
|
data class Submit(
|
||||||
|
val isAccountCreation: Boolean,
|
||||||
|
val homeserverUrl: String,
|
||||||
|
val resolvedHomeserverUrl: String?,
|
||||||
|
val loginHint: String?,
|
||||||
|
) : LoginModeEvent
|
||||||
|
|
||||||
|
data object ClearError : LoginModeEvent
|
||||||
|
|
||||||
|
data object RequestLocalNetworkPermission : LoginModeEvent
|
||||||
|
|
||||||
|
data object DismissLocalNetworkPermission : LoginModeEvent
|
||||||
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.login
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||||
|
import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter
|
||||||
|
import io.element.android.features.login.impl.screens.classic.loginwithclassic.LoginWithClassicPresenter
|
||||||
|
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
|
||||||
|
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
import io.element.android.libraries.matrix.api.auth.OAuthPrompt
|
||||||
|
import io.element.android.libraries.oauth.api.OAuthAction
|
||||||
|
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presenter responsible for managing the login flow, including handling OAuth actions and
|
||||||
|
* submitting login requests.
|
||||||
|
* It's a helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter],
|
||||||
|
* [ChooseAccountProviderPresenter] and [LoginWithClassicPresenter].
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
class LoginModePresenter(
|
||||||
|
private val oAuthActionFlow: OAuthActionFlow,
|
||||||
|
private val authenticationService: MatrixAuthenticationService,
|
||||||
|
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
|
||||||
|
private val localNetworkPermissionGate: LocalNetworkPermissionGate,
|
||||||
|
) : Presenter<LoginModeState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): LoginModeState {
|
||||||
|
val loginMode: MutableState<AsyncData<LoginMode>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||||
|
|
||||||
|
val gateState = localNetworkPermissionGate.present<LoginModeEvent.Submit>(
|
||||||
|
urlOf = { it.homeserverUrl },
|
||||||
|
onProceed = { request -> performSubmit(request, loginMode) },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Forward OAuth navigation events to the presenter's own loginMode state.
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
oAuthActionFlow.collect { action ->
|
||||||
|
if (action != null) handleOAuthAction(action, loginMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvent(event: LoginModeEvent) {
|
||||||
|
when (event) {
|
||||||
|
LoginModeEvent.ClearError -> loginMode.value = AsyncData.Uninitialized
|
||||||
|
is LoginModeEvent.Submit -> gateState.submit(event)
|
||||||
|
LoginModeEvent.DismissLocalNetworkPermission -> gateState.abort()
|
||||||
|
LoginModeEvent.RequestLocalNetworkPermission -> gateState.requestPermission()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return LoginModeState(
|
||||||
|
loginMode = loginMode.value,
|
||||||
|
localNetworkPermissionDialog = gateState.dialog,
|
||||||
|
eventSink = ::handleEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun performSubmit(request: LoginModeEvent.Submit, loginMode: MutableState<AsyncData<LoginMode>>) {
|
||||||
|
suspend {
|
||||||
|
authenticationService.setHomeserver(request.homeserverUrl).recoverCatching {
|
||||||
|
// Fallback to the well-known-resolved URL if the primary URL failed and the caller supplied one.
|
||||||
|
if (request.resolvedHomeserverUrl != null && request.resolvedHomeserverUrl != request.homeserverUrl) {
|
||||||
|
authenticationService.setHomeserver(request.resolvedHomeserverUrl).getOrThrow()
|
||||||
|
} else {
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}.map { matrixHomeServerDetails ->
|
||||||
|
when {
|
||||||
|
matrixHomeServerDetails.supportsOAuthLogin -> {
|
||||||
|
val oAuthPrompt = if (request.isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login
|
||||||
|
LoginMode.OAuth(
|
||||||
|
authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = request.loginHint).getOrThrow()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
request.isAccountCreation -> LoginMode.AccountCreation(
|
||||||
|
webClientUrlForAuthenticationRetriever.retrieve(request.homeserverUrl)
|
||||||
|
)
|
||||||
|
matrixHomeServerDetails.supportsPasswordLogin -> LoginMode.PasswordLogin
|
||||||
|
else -> error("Unsupported login flow")
|
||||||
|
}
|
||||||
|
}.getOrThrow()
|
||||||
|
}.runCatchingUpdatingState(
|
||||||
|
state = loginMode,
|
||||||
|
errorTransform = {
|
||||||
|
when (it) {
|
||||||
|
is AccountCreationNotSupported -> it
|
||||||
|
else -> ChangeServerError.from(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleOAuthAction(action: OAuthAction, loginMode: MutableState<AsyncData<LoginMode>>) {
|
||||||
|
if (action is OAuthAction.GoBack && action.toUnblock && loginMode.value !is AsyncData.Loading) {
|
||||||
|
// Ignore GoBack that isn't tied to an in-flight login — it comes from LoginFlowNode
|
||||||
|
// when the user backs out after a completed login attempt.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loginMode.value = AsyncData.Loading()
|
||||||
|
when (action) {
|
||||||
|
is OAuthAction.GoBack -> authenticationService.cancelOAuthLogin()
|
||||||
|
.onSuccess { loginMode.value = AsyncData.Uninitialized }
|
||||||
|
.onFailure { loginMode.value = AsyncData.Failure(it) }
|
||||||
|
is OAuthAction.Success -> authenticationService.loginWithOAuth(action.url)
|
||||||
|
.onFailure { loginMode.value = AsyncData.Failure(it) }
|
||||||
|
}
|
||||||
|
oAuthActionFlow.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-2
@@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
@@ -8,7 +7,15 @@
|
|||||||
|
|
||||||
package io.element.android.features.login.impl.login
|
package io.element.android.features.login.impl.login
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
|
|
||||||
|
data class LoginModeState(
|
||||||
|
val loginMode: AsyncData<LoginMode>,
|
||||||
|
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
|
||||||
|
val eventSink: (LoginModeEvent) -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
sealed interface LoginMode {
|
sealed interface LoginMode {
|
||||||
data object PasswordLogin : LoginMode
|
data object PasswordLogin : LoginMode
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.login
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
|
|
||||||
|
fun aLoginModeState(
|
||||||
|
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||||
|
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
|
||||||
|
eventSink: (LoginModeEvent) -> Unit = {},
|
||||||
|
) = LoginModeState(
|
||||||
|
loginMode = loginMode,
|
||||||
|
localNetworkPermissionDialog = localNetworkPermissionDialog,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
+11
-5
@@ -14,6 +14,7 @@ import io.element.android.libraries.core.coroutine.parallelMap
|
|||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
import io.element.android.libraries.core.uri.isValidUrl
|
import io.element.android.libraries.core.uri.isValidUrl
|
||||||
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
@@ -28,6 +29,7 @@ import java.util.Collections
|
|||||||
class HomeserverResolver(
|
class HomeserverResolver(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker,
|
private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker,
|
||||||
|
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
|
||||||
) {
|
) {
|
||||||
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
|
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
|
||||||
val flowContext = currentCoroutineContext()
|
val flowContext = currentCoroutineContext()
|
||||||
@@ -39,13 +41,17 @@ class HomeserverResolver(
|
|||||||
// Run all the requests in parallel
|
// Run all the requests in parallel
|
||||||
withContext(dispatchers.io) {
|
withContext(dispatchers.io) {
|
||||||
list.parallelMap { url ->
|
list.parallelMap { url ->
|
||||||
val isValid = homeServerLoginCompatibilityChecker.check(url)
|
// Skip the compatibility probe if we'd need ACCESS_LOCAL_NETWORK first —
|
||||||
.onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") }
|
// otherwise the probe hangs on the TCP timeout for ~30s. Emit the URL as a
|
||||||
.getOrNull()
|
// candidate directly; the actual sign-in flow triggers the permission prompt.
|
||||||
?: return@parallelMap
|
val shouldRequestPermissionOrIsValid = localNetworkPermissionAdvisor.shouldRequestPermissionFor(url) ||
|
||||||
|
homeServerLoginCompatibilityChecker.check(url)
|
||||||
|
.onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") }
|
||||||
|
.getOrNull()
|
||||||
|
?: return@parallelMap
|
||||||
|
|
||||||
// Emit the list as soon as possible
|
// Emit the list as soon as possible
|
||||||
if (isValid) {
|
if (shouldRequestPermissionOrIsValid) {
|
||||||
currentList.add(HomeserverData(homeserverUrl = url))
|
currentList.add(HomeserverData(homeserverUrl = url))
|
||||||
withContext(flowContext) {
|
withContext(flowContext) {
|
||||||
emit(currentList.toList())
|
emit(currentList.toList())
|
||||||
|
|||||||
+17
-18
@@ -12,50 +12,49 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.appconfig.AuthenticationConfig
|
import io.element.android.appconfig.AuthenticationConfig
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||||
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class ChooseAccountProviderPresenter(
|
class ChooseAccountProviderPresenter(
|
||||||
private val enterpriseService: EnterpriseService,
|
private val enterpriseService: EnterpriseService,
|
||||||
private val loginHelper: LoginHelper,
|
private val loginModePresenter: Presenter<LoginModeState>,
|
||||||
) : Presenter<ChooseAccountProviderState> {
|
) : Presenter<ChooseAccountProviderState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ChooseAccountProviderState {
|
override fun present(): ChooseAccountProviderState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val loginModeState = loginModePresenter.present()
|
||||||
val loginMode by loginHelper.collectLoginMode()
|
|
||||||
|
|
||||||
var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) }
|
var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
fun handleEvent(event: ChooseAccountProviderEvents) {
|
fun handleEvent(event: ChooseAccountProviderEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
ChooseAccountProviderEvents.Continue -> localCoroutineScope.launch {
|
ChooseAccountProviderEvents.Continue -> {
|
||||||
selectedAccountProvider?.let {
|
selectedAccountProvider?.let { provider ->
|
||||||
loginHelper.submit(
|
loginModeState.eventSink(
|
||||||
isAccountCreation = false,
|
LoginModeEvent.Submit(
|
||||||
homeserverUrl = it.url,
|
isAccountCreation = false,
|
||||||
resolvedHomeserverUrl = null,
|
homeserverUrl = provider.url,
|
||||||
loginHint = null,
|
resolvedHomeserverUrl = null,
|
||||||
|
loginHint = null,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ChooseAccountProviderEvents.SelectAccountProvider -> {
|
is ChooseAccountProviderEvents.SelectAccountProvider -> {
|
||||||
// Ensure that the user do not change the server during processing
|
// Ensure that the user does not change the server during processing.
|
||||||
if (loginMode is AsyncData.Uninitialized) {
|
if (loginModeState.loginMode is AsyncData.Uninitialized) {
|
||||||
selectedAccountProvider = event.accountProvider
|
selectedAccountProvider = event.accountProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChooseAccountProviderEvents.ClearError -> loginHelper.clearError()
|
ChooseAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +76,7 @@ class ChooseAccountProviderPresenter(
|
|||||||
return ChooseAccountProviderState(
|
return ChooseAccountProviderState(
|
||||||
accountProviders = staticAccountProviderList,
|
accountProviders = staticAccountProviderList,
|
||||||
selectedAccountProvider = selectedAccountProvider,
|
selectedAccountProvider = selectedAccountProvider,
|
||||||
loginMode = loginMode,
|
loginModeState = loginModeState,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -9,16 +9,17 @@
|
|||||||
package io.element.android.features.login.impl.screens.chooseaccountprovider
|
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.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ChooseAccountProviderState(
|
data class ChooseAccountProviderState(
|
||||||
val accountProviders: ImmutableList<AccountProvider>,
|
val accountProviders: ImmutableList<AccountProvider>,
|
||||||
val selectedAccountProvider: AccountProvider?,
|
val selectedAccountProvider: AccountProvider?,
|
||||||
val loginMode: AsyncData<LoginMode>,
|
val loginModeState: LoginModeState,
|
||||||
val eventSink: (ChooseAccountProviderEvents) -> Unit,
|
val eventSink: (ChooseAccountProviderEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val submitEnabled: Boolean
|
val submitEnabled: Boolean
|
||||||
get() = selectedAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
|
get() = selectedAccountProvider != null &&
|
||||||
|
(loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -11,7 +11,8 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
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.accountprovider.anAccountProvider
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
|
import io.element.android.features.login.impl.login.aLoginModeState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ open class ChooseAccountProviderStateProvider : PreviewParameterProvider<ChooseA
|
|||||||
server3,
|
server3,
|
||||||
),
|
),
|
||||||
selectedAccountProvider = server2,
|
selectedAccountProvider = server2,
|
||||||
loginMode = AsyncData.Loading(),
|
loginModeState = aLoginModeState(loginMode = AsyncData.Loading()),
|
||||||
),
|
),
|
||||||
// Add other state here
|
// Add other state here
|
||||||
)
|
)
|
||||||
@@ -69,11 +70,11 @@ fun aChooseAccountProviderState(
|
|||||||
anAccountProvider()
|
anAccountProvider()
|
||||||
),
|
),
|
||||||
selectedAccountProvider: AccountProvider? = null,
|
selectedAccountProvider: AccountProvider? = null,
|
||||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
loginModeState: LoginModeState = aLoginModeState(),
|
||||||
eventSink: (ChooseAccountProviderEvents) -> Unit = {},
|
eventSink: (ChooseAccountProviderEvents) -> Unit = {},
|
||||||
) = ChooseAccountProviderState(
|
) = ChooseAccountProviderState(
|
||||||
accountProviders = accountProviders.toImmutableList(),
|
accountProviders = accountProviders.toImmutableList(),
|
||||||
selectedAccountProvider = selectedAccountProvider,
|
selectedAccountProvider = selectedAccountProvider,
|
||||||
loginMode = loginMode,
|
loginModeState = loginModeState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|||||||
+14
-3
@@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderView
|
import io.element.android.features.login.impl.accountprovider.AccountProviderView
|
||||||
|
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||||
import io.element.android.features.login.impl.login.LoginModeView
|
import io.element.android.features.login.impl.login.LoginModeView
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
@@ -44,6 +45,7 @@ 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.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -56,9 +58,9 @@ fun ChooseAccountProviderView(
|
|||||||
onCreateAccountContinue: (url: String) -> Unit,
|
onCreateAccountContinue: (url: String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val isLoading by remember(state.loginMode) {
|
val isLoading by remember(state.loginModeState.loginMode) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
state.loginMode is AsyncData.Loading
|
state.loginModeState.loginMode is AsyncData.Loading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +126,7 @@ fun ChooseAccountProviderView(
|
|||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
}
|
}
|
||||||
LoginModeView(
|
LoginModeView(
|
||||||
loginMode = state.loginMode,
|
loginMode = state.loginModeState.loginMode,
|
||||||
onClearError = {
|
onClearError = {
|
||||||
state.eventSink(ChooseAccountProviderEvents.ClearError)
|
state.eventSink(ChooseAccountProviderEvents.ClearError)
|
||||||
},
|
},
|
||||||
@@ -135,6 +137,15 @@ fun ChooseAccountProviderView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LocalNetworkPermissionDialogView(
|
||||||
|
dialog = state.loginModeState.localNetworkPermissionDialog,
|
||||||
|
onSubmit = {
|
||||||
|
state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission)
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
|
|||||||
+13
-10
@@ -20,7 +20,8 @@ import dev.zacsweers.metro.AssistedInject
|
|||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.classic.ElementClassicConnection
|
import io.element.android.features.login.impl.classic.ElementClassicConnection
|
||||||
import io.element.android.features.login.impl.classic.ElementClassicConnectionState
|
import io.element.android.features.login.impl.classic.ElementClassicConnectionState
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||||
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
@@ -32,7 +33,7 @@ import kotlinx.coroutines.launch
|
|||||||
class LoginWithClassicPresenter(
|
class LoginWithClassicPresenter(
|
||||||
@Assisted private val userId: UserId,
|
@Assisted private val userId: UserId,
|
||||||
@Assisted private val navigator: LoginWithClassicNavigator,
|
@Assisted private val navigator: LoginWithClassicNavigator,
|
||||||
private val loginHelper: LoginHelper,
|
private val loginModePresenter: Presenter<LoginModeState>,
|
||||||
private val elementClassicConnection: ElementClassicConnection,
|
private val elementClassicConnection: ElementClassicConnection,
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
@@ -51,7 +52,7 @@ class LoginWithClassicPresenter(
|
|||||||
var loginWithClassicAction by remember {
|
var loginWithClassicAction by remember {
|
||||||
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
val loginMode by loginHelper.collectLoginMode()
|
val loginModeState = loginModePresenter.present()
|
||||||
val elementClassicConnectionState by elementClassicConnection.stateFlow.collectAsState()
|
val elementClassicConnectionState by elementClassicConnection.stateFlow.collectAsState()
|
||||||
|
|
||||||
fun handleEvent(event: LoginWithClassicEvent) {
|
fun handleEvent(event: LoginWithClassicEvent) {
|
||||||
@@ -69,11 +70,13 @@ class LoginWithClassicPresenter(
|
|||||||
val elementClassicUserId = currentState.elementClassicSession.userId
|
val elementClassicUserId = currentState.elementClassicSession.userId
|
||||||
val accountProvider = elementClassicUserId.domainName.orEmpty().ensureProtocol()
|
val accountProvider = elementClassicUserId.domainName.orEmpty().ensureProtocol()
|
||||||
accountProviderDataSource.setUrl(accountProvider)
|
accountProviderDataSource.setUrl(accountProvider)
|
||||||
loginHelper.submit(
|
loginModeState.eventSink(
|
||||||
isAccountCreation = false,
|
LoginModeEvent.Submit(
|
||||||
homeserverUrl = accountProvider,
|
isAccountCreation = false,
|
||||||
resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl,
|
homeserverUrl = accountProvider,
|
||||||
loginHint = "mxid:" + elementClassicUserId.value,
|
resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl,
|
||||||
|
loginHint = "mxid:" + elementClassicUserId.value,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +86,7 @@ class LoginWithClassicPresenter(
|
|||||||
}
|
}
|
||||||
LoginWithClassicEvent.ClearError -> {
|
LoginWithClassicEvent.ClearError -> {
|
||||||
loginWithClassicAction = AsyncAction.Uninitialized
|
loginWithClassicAction = AsyncAction.Uninitialized
|
||||||
loginHelper.clearError()
|
loginModeState.eventSink(LoginModeEvent.ClearError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +97,7 @@ class LoginWithClassicPresenter(
|
|||||||
userId = userId,
|
userId = userId,
|
||||||
displayName = elementClassicReady?.displayName,
|
displayName = elementClassicReady?.displayName,
|
||||||
avatar = elementClassicReady?.avatar,
|
avatar = elementClassicReady?.avatar,
|
||||||
loginMode = loginMode,
|
loginModeState = loginModeState,
|
||||||
loginWithClassicAction = loginWithClassicAction,
|
loginWithClassicAction = loginWithClassicAction,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-3
@@ -9,9 +9,8 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic
|
|||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@@ -21,6 +20,6 @@ data class LoginWithClassicState(
|
|||||||
val displayName: String?,
|
val displayName: String?,
|
||||||
val avatar: Bitmap?,
|
val avatar: Bitmap?,
|
||||||
val loginWithClassicAction: AsyncAction<Unit>,
|
val loginWithClassicAction: AsyncAction<Unit>,
|
||||||
val loginMode: AsyncData<LoginMode>,
|
val loginModeState: LoginModeState,
|
||||||
val eventSink: (LoginWithClassicEvent) -> Unit,
|
val eventSink: (LoginWithClassicEvent) -> Unit,
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-4
@@ -9,9 +9,9 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic
|
|||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
|
import io.element.android.features.login.impl.login.aLoginModeState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ fun aLoginWithClassicState(
|
|||||||
displayName: String? = null,
|
displayName: String? = null,
|
||||||
avatar: Bitmap? = null,
|
avatar: Bitmap? = null,
|
||||||
loginWithClassicAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
loginWithClassicAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
loginModeState: LoginModeState = aLoginModeState(),
|
||||||
eventSink: (LoginWithClassicEvent) -> Unit = {},
|
eventSink: (LoginWithClassicEvent) -> Unit = {},
|
||||||
) = LoginWithClassicState(
|
) = LoginWithClassicState(
|
||||||
isElementPro = isElementPro,
|
isElementPro = isElementPro,
|
||||||
@@ -37,6 +37,6 @@ fun aLoginWithClassicState(
|
|||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
avatar = avatar,
|
avatar = avatar,
|
||||||
loginWithClassicAction = loginWithClassicAction,
|
loginWithClassicAction = loginWithClassicAction,
|
||||||
loginMode = loginMode,
|
loginModeState = loginModeState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|||||||
+14
-3
@@ -35,6 +35,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
|
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||||
import io.element.android.features.login.impl.login.LoginModeView
|
import io.element.android.features.login.impl.login.LoginModeView
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||||
@@ -50,6 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.Button
|
|||||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -65,9 +67,9 @@ fun LoginWithClassicView(
|
|||||||
onCreateAccountContinue: (url: String) -> Unit,
|
onCreateAccountContinue: (url: String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val isLoading by remember(state.loginMode) {
|
val isLoading by remember(state.loginModeState.loginMode) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
state.loginMode is AsyncData.Loading
|
state.loginModeState.loginMode is AsyncData.Loading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +197,7 @@ fun LoginWithClassicView(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
LoginModeView(
|
LoginModeView(
|
||||||
loginMode = state.loginMode,
|
loginMode = state.loginModeState.loginMode,
|
||||||
onClearError = {
|
onClearError = {
|
||||||
state.eventSink(LoginWithClassicEvent.ClearError)
|
state.eventSink(LoginWithClassicEvent.ClearError)
|
||||||
},
|
},
|
||||||
@@ -204,6 +206,15 @@ fun LoginWithClassicView(
|
|||||||
onNeedLoginPassword = onNeedLoginPassword,
|
onNeedLoginPassword = onNeedLoginPassword,
|
||||||
onCreateAccountContinue = onCreateAccountContinue,
|
onCreateAccountContinue = onCreateAccountContinue,
|
||||||
)
|
)
|
||||||
|
LocalNetworkPermissionDialogView(
|
||||||
|
dialog = state.loginModeState.localNetworkPermissionDialog,
|
||||||
|
onSubmit = {
|
||||||
|
state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission)
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
|
|||||||
+9
-12
@@ -11,20 +11,19 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||||
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class ConfirmAccountProviderPresenter(
|
class ConfirmAccountProviderPresenter(
|
||||||
@Assisted private val params: Params,
|
@Assisted private val params: Params,
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
private val loginHelper: LoginHelper,
|
private val loginModePresenter: Presenter<LoginModeState>,
|
||||||
) : Presenter<ConfirmAccountProviderState> {
|
) : Presenter<ConfirmAccountProviderState> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val isAccountCreation: Boolean,
|
val isAccountCreation: Boolean,
|
||||||
@@ -38,28 +37,26 @@ class ConfirmAccountProviderPresenter(
|
|||||||
@Composable
|
@Composable
|
||||||
override fun present(): ConfirmAccountProviderState {
|
override fun present(): ConfirmAccountProviderState {
|
||||||
val accountProvider by accountProviderDataSource.flow.collectAsState()
|
val accountProvider by accountProviderDataSource.flow.collectAsState()
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val loginModeState = loginModePresenter.present()
|
||||||
|
|
||||||
val loginMode by loginHelper.collectLoginMode()
|
|
||||||
|
|
||||||
fun handleEvent(event: ConfirmAccountProviderEvents) {
|
fun handleEvent(event: ConfirmAccountProviderEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
ConfirmAccountProviderEvents.Continue -> localCoroutineScope.launch {
|
ConfirmAccountProviderEvents.Continue -> loginModeState.eventSink(
|
||||||
loginHelper.submit(
|
LoginModeEvent.Submit(
|
||||||
isAccountCreation = params.isAccountCreation,
|
isAccountCreation = params.isAccountCreation,
|
||||||
homeserverUrl = accountProvider.url,
|
homeserverUrl = accountProvider.url,
|
||||||
resolvedHomeserverUrl = null,
|
resolvedHomeserverUrl = null,
|
||||||
loginHint = null,
|
loginHint = null,
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
ConfirmAccountProviderEvents.ClearError -> loginHelper.clearError()
|
ConfirmAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConfirmAccountProviderState(
|
return ConfirmAccountProviderState(
|
||||||
accountProvider = accountProvider,
|
accountProvider = accountProvider,
|
||||||
isAccountCreation = params.isAccountCreation,
|
isAccountCreation = params.isAccountCreation,
|
||||||
loginMode = loginMode,
|
loginModeState = loginModeState,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -9,14 +9,16 @@
|
|||||||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||||
|
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
data class ConfirmAccountProviderState(
|
data class ConfirmAccountProviderState(
|
||||||
val accountProvider: AccountProvider,
|
val accountProvider: AccountProvider,
|
||||||
val isAccountCreation: Boolean,
|
val isAccountCreation: Boolean,
|
||||||
val loginMode: AsyncData<LoginMode>,
|
val loginModeState: LoginModeState,
|
||||||
val eventSink: (ConfirmAccountProviderEvents) -> Unit
|
val eventSink: (ConfirmAccountProviderEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
|
val submitEnabled: Boolean
|
||||||
|
get() = accountProvider.url.isNotEmpty() &&
|
||||||
|
(loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -11,7 +11,8 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
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.accountprovider.anAccountProvider
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
|
import io.element.android.features.login.impl.login.aLoginModeState
|
||||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
|
|||||||
),
|
),
|
||||||
aConfirmAccountProviderState(
|
aConfirmAccountProviderState(
|
||||||
isAccountCreation = true,
|
isAccountCreation = true,
|
||||||
loginMode = AsyncData.Failure(AccountCreationNotSupported())
|
loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AccountCreationNotSupported())),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -32,11 +33,11 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
|
|||||||
private fun aConfirmAccountProviderState(
|
private fun aConfirmAccountProviderState(
|
||||||
accountProvider: AccountProvider = anAccountProvider(),
|
accountProvider: AccountProvider = anAccountProvider(),
|
||||||
isAccountCreation: Boolean = false,
|
isAccountCreation: Boolean = false,
|
||||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
loginModeState: LoginModeState = aLoginModeState(),
|
||||||
eventSink: (ConfirmAccountProviderEvents) -> Unit = {},
|
eventSink: (ConfirmAccountProviderEvents) -> Unit = {},
|
||||||
) = ConfirmAccountProviderState(
|
) = ConfirmAccountProviderState(
|
||||||
accountProvider = accountProvider,
|
accountProvider = accountProvider,
|
||||||
isAccountCreation = isAccountCreation,
|
isAccountCreation = isAccountCreation,
|
||||||
loginMode = loginMode,
|
loginModeState = loginModeState,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|||||||
+15
-3
@@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
|
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||||
import io.element.android.features.login.impl.login.LoginModeView
|
import io.element.android.features.login.impl.login.LoginModeView
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||||
@@ -31,6 +32,7 @@ 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.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -45,9 +47,9 @@ fun ConfirmAccountProviderView(
|
|||||||
onChange: () -> Unit,
|
onChange: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val isLoading by remember(state.loginMode) {
|
val isLoading by remember(state.loginModeState.loginMode) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
state.loginMode is AsyncData.Loading
|
state.loginModeState.loginMode is AsyncData.Loading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val eventSink = state.eventSink
|
val eventSink = state.eventSink
|
||||||
@@ -98,7 +100,7 @@ fun ConfirmAccountProviderView(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
LoginModeView(
|
LoginModeView(
|
||||||
loginMode = state.loginMode,
|
loginMode = state.loginModeState.loginMode,
|
||||||
onClearError = {
|
onClearError = {
|
||||||
eventSink(ConfirmAccountProviderEvents.ClearError)
|
eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||||
},
|
},
|
||||||
@@ -108,6 +110,16 @@ fun ConfirmAccountProviderView(
|
|||||||
onCreateAccountContinue = onCreateAccountContinue,
|
onCreateAccountContinue = onCreateAccountContinue,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocalNetworkPermissionDialogView(
|
||||||
|
dialog = state.loginModeState.localNetworkPermissionDialog,
|
||||||
|
onSubmit = {
|
||||||
|
state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission)
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
|
|||||||
+13
-10
@@ -25,7 +25,8 @@ import io.element.android.features.enterprise.api.EnterpriseService
|
|||||||
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
|
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
|
||||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||||
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
@@ -41,7 +42,7 @@ class OnBoardingPresenter(
|
|||||||
private val enterpriseService: EnterpriseService,
|
private val enterpriseService: EnterpriseService,
|
||||||
private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl,
|
private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl,
|
||||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||||
private val loginHelper: LoginHelper,
|
private val loginModePresenter: Presenter<LoginModeState>,
|
||||||
private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider,
|
private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider,
|
||||||
private val sessionStore: SessionStore,
|
private val sessionStore: SessionStore,
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
@@ -98,21 +99,23 @@ class OnBoardingPresenter(
|
|||||||
value = sessionStore.numberOfSessions() > 0
|
value = sessionStore.numberOfSessions() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
val loginMode by loginHelper.collectLoginMode()
|
val loginModeState = loginModePresenter.present()
|
||||||
|
|
||||||
fun handleEvent(event: OnBoardingEvents) {
|
fun handleEvent(event: OnBoardingEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch {
|
is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch {
|
||||||
// Ensure that the current account provider is set
|
// Ensure that the current account provider is set
|
||||||
accountProviderDataSource.setUrl(event.defaultAccountProvider)
|
accountProviderDataSource.setUrl(event.defaultAccountProvider)
|
||||||
loginHelper.submit(
|
loginModeState.eventSink(
|
||||||
isAccountCreation = false,
|
LoginModeEvent.Submit(
|
||||||
homeserverUrl = event.defaultAccountProvider,
|
isAccountCreation = false,
|
||||||
resolvedHomeserverUrl = null,
|
homeserverUrl = event.defaultAccountProvider,
|
||||||
loginHint = params.loginHint?.takeIf { forcedAccountProvider == null },
|
resolvedHomeserverUrl = null,
|
||||||
|
loginHint = params.loginHint?.takeIf { forcedAccountProvider == null },
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OnBoardingEvents.ClearError -> loginHelper.clearError()
|
OnBoardingEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError)
|
||||||
OnBoardingEvents.OnVersionClick -> {
|
OnBoardingEvents.OnVersionClick -> {
|
||||||
if (canReportBug) {
|
if (canReportBug) {
|
||||||
if (multipleTapToUnlock.unlock(localCoroutineScope)) {
|
if (multipleTapToUnlock.unlock(localCoroutineScope)) {
|
||||||
@@ -133,7 +136,7 @@ class OnBoardingPresenter(
|
|||||||
canLoginWithQrCode = canLoginWithQrCode,
|
canLoginWithQrCode = canLoginWithQrCode,
|
||||||
canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
||||||
canReportBug = canReportBug && showReportBug,
|
canReportBug = canReportBug && showReportBug,
|
||||||
loginMode = loginMode,
|
loginModeState = loginModeState,
|
||||||
version = buildMeta.versionName,
|
version = buildMeta.versionName,
|
||||||
onBoardingLogoResId = onBoardingLogoResId,
|
onBoardingLogoResId = onBoardingLogoResId,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
|
|||||||
+4
-3
@@ -9,7 +9,7 @@
|
|||||||
package io.element.android.features.login.impl.screens.onboarding
|
package io.element.android.features.login.impl.screens.onboarding
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
data class OnBoardingState(
|
data class OnBoardingState(
|
||||||
@@ -25,9 +25,10 @@ data class OnBoardingState(
|
|||||||
val version: String,
|
val version: String,
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
val onBoardingLogoResId: Int?,
|
val onBoardingLogoResId: Int?,
|
||||||
val loginMode: AsyncData<LoginMode>,
|
val loginModeState: LoginModeState,
|
||||||
val eventSink: (OnBoardingEvents) -> Unit,
|
val eventSink: (OnBoardingEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val submitEnabled: Boolean
|
val submitEnabled: Boolean
|
||||||
get() = defaultAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
|
get() = defaultAccountProvider != null &&
|
||||||
|
(loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -10,8 +10,8 @@ package io.element.android.features.login.impl.screens.onboarding
|
|||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginModeState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.features.login.impl.login.aLoginModeState
|
||||||
import io.element.android.libraries.designsystem.R
|
import io.element.android.libraries.designsystem.R
|
||||||
|
|
||||||
open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
|
open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
|
||||||
@@ -49,7 +49,7 @@ fun anOnBoardingState(
|
|||||||
version: String = "1.0.0",
|
version: String = "1.0.0",
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
customLogoResId: Int? = null,
|
customLogoResId: Int? = null,
|
||||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
loginModeState: LoginModeState = aLoginModeState(),
|
||||||
eventSink: (OnBoardingEvents) -> Unit = {},
|
eventSink: (OnBoardingEvents) -> Unit = {},
|
||||||
) = OnBoardingState(
|
) = OnBoardingState(
|
||||||
isAddingAccount = isAddingAccount,
|
isAddingAccount = isAddingAccount,
|
||||||
@@ -62,7 +62,7 @@ fun anOnBoardingState(
|
|||||||
canCreateAccount = canCreateAccount,
|
canCreateAccount = canCreateAccount,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
version = version,
|
version = version,
|
||||||
loginMode = loginMode,
|
loginModeState = loginModeState,
|
||||||
onBoardingLogoResId = customLogoResId,
|
onBoardingLogoResId = customLogoResId,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|||||||
+14
-3
@@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
|
import io.element.android.features.login.impl.login.LoginModeEvent
|
||||||
import io.element.android.features.login.impl.login.LoginModeView
|
import io.element.android.features.login.impl.login.LoginModeView
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
|
||||||
@@ -50,6 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.IconSource
|
|||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -74,7 +76,7 @@ fun OnBoardingView(
|
|||||||
) {
|
) {
|
||||||
val loginView = @Composable {
|
val loginView = @Composable {
|
||||||
LoginModeView(
|
LoginModeView(
|
||||||
loginMode = state.loginMode,
|
loginMode = state.loginModeState.loginMode,
|
||||||
onClearError = {
|
onClearError = {
|
||||||
state.eventSink(OnBoardingEvents.ClearError)
|
state.eventSink(OnBoardingEvents.ClearError)
|
||||||
},
|
},
|
||||||
@@ -83,6 +85,15 @@ fun OnBoardingView(
|
|||||||
onNeedLoginPassword = onNeedLoginPassword,
|
onNeedLoginPassword = onNeedLoginPassword,
|
||||||
onCreateAccountContinue = onCreateAccountContinue,
|
onCreateAccountContinue = onCreateAccountContinue,
|
||||||
)
|
)
|
||||||
|
LocalNetworkPermissionDialogView(
|
||||||
|
dialog = state.loginModeState.localNetworkPermissionDialog,
|
||||||
|
onSubmit = {
|
||||||
|
state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission)
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val buttons = @Composable {
|
val buttons = @Composable {
|
||||||
OnBoardingButtons(
|
OnBoardingButtons(
|
||||||
@@ -260,9 +271,9 @@ private fun OnBoardingButtons(
|
|||||||
onCreateAccount: () -> Unit,
|
onCreateAccount: () -> Unit,
|
||||||
onReportProblem: () -> Unit,
|
onReportProblem: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val isLoading by remember(state.loginMode) {
|
val isLoading by remember(state.loginModeState.loginMode) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
state.loginMode is AsyncData.Loading
|
state.loginModeState.loginMode is AsyncData.Loading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,8 @@
|
|||||||
<string name="screen_qr_code_login_connecting_subtitle">"برقرار کدن اتّصالی امن"</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_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_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_list_item_3">"ورود دستی در صورت کار نکردنش"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"اتّصال ناامن"</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_subtitle">"از شما خواسته خواهد شد که دو رقم نشان داده روی این افزاره را وارد کنید."</string>
|
||||||
|
|||||||
@@ -37,11 +37,20 @@
|
|||||||
<string name="screen_login_subtitle">"Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi."</string>
|
<string name="screen_login_subtitle">"Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi."</string>
|
||||||
<string name="screen_login_title">"Selamat datang kembali!"</string>
|
<string name="screen_login_title">"Selamat datang kembali!"</string>
|
||||||
<string name="screen_login_title_with_homeserver">"Masuk ke %1$s"</string>
|
<string name="screen_login_title_with_homeserver">"Masuk ke %1$s"</string>
|
||||||
|
<string name="screen_missing_key_backup_open_element_classic">"Buka Element Classic"</string>
|
||||||
|
<string name="screen_missing_key_backup_step_1">"Buka Element Classic di perangkat Anda"</string>
|
||||||
|
<string name="screen_missing_key_backup_step_2_android">"Buka Pengaturan > Keamanan &amp; Privasi"</string>
|
||||||
|
<string name="screen_missing_key_backup_step_3_android">"Dalam pengelolaan kunci kriptografi, pilih Pemulihan pesan terenkripsi"</string>
|
||||||
|
<string name="screen_missing_key_backup_step_4">"Ikuti petunjuk berikut untuk mengaktifkan penyimpanan kunci Anda"</string>
|
||||||
|
<string name="screen_missing_key_backup_step_5">"Kembali ke %1$s"</string>
|
||||||
|
<string name="screen_missing_key_backup_title">"Aktifkan penyimpanan kunci Anda sebelum melanjutkan ke %1$s"</string>
|
||||||
<string name="screen_onboarding_app_version">"Versi %1$s"</string>
|
<string name="screen_onboarding_app_version">"Versi %1$s"</string>
|
||||||
|
<string name="screen_onboarding_checking_account">"Memeriksa akun…"</string>
|
||||||
<string name="screen_onboarding_sign_in_manually">"Masuk secara manual"</string>
|
<string name="screen_onboarding_sign_in_manually">"Masuk secara manual"</string>
|
||||||
<string name="screen_onboarding_sign_in_to">"Masuk ke %1$s"</string>
|
<string name="screen_onboarding_sign_in_to">"Masuk ke %1$s"</string>
|
||||||
<string name="screen_onboarding_sign_in_with_qr_code">"Masuk dengan kode QR"</string>
|
<string name="screen_onboarding_sign_in_with_qr_code">"Masuk dengan kode QR"</string>
|
||||||
<string name="screen_onboarding_sign_up">"Buat akun"</string>
|
<string name="screen_onboarding_sign_up">"Buat akun"</string>
|
||||||
|
<string name="screen_onboarding_welcome_back">"Selamat datang kembali!"</string>
|
||||||
<string name="screen_onboarding_welcome_message">"Selamat datang di %1$s tercepat yang pernah ada. Berdaya besar untuk kecepatan dan kesederhanaan."</string>
|
<string name="screen_onboarding_welcome_message">"Selamat datang di %1$s tercepat yang pernah ada. Berdaya besar untuk kecepatan dan kesederhanaan."</string>
|
||||||
<string name="screen_onboarding_welcome_subtitle">"Selamat datang di %1$s. Berdaya penuh, untuk kecepatan dan kesederhanaan."</string>
|
<string name="screen_onboarding_welcome_subtitle">"Selamat datang di %1$s. Berdaya penuh, untuk kecepatan dan kesederhanaan."</string>
|
||||||
<string name="screen_onboarding_welcome_title">"Berada di elemen Anda"</string>
|
<string name="screen_onboarding_welcome_title">"Berada di elemen Anda"</string>
|
||||||
@@ -60,6 +69,8 @@
|
|||||||
<string name="screen_qr_code_login_error_cancelled_title">"Permintaan masuk dibatalkan"</string>
|
<string name="screen_qr_code_login_error_cancelled_title">"Permintaan masuk dibatalkan"</string>
|
||||||
<string name="screen_qr_code_login_error_declined_subtitle">"Proses masuk ditolak di perangkat lain."</string>
|
<string name="screen_qr_code_login_error_declined_subtitle">"Proses masuk ditolak di perangkat lain."</string>
|
||||||
<string name="screen_qr_code_login_error_declined_title">"Proses masuk ditolak"</string>
|
<string name="screen_qr_code_login_error_declined_title">"Proses masuk ditolak"</string>
|
||||||
|
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Anda tidak perlu melakukan hal lain."</string>
|
||||||
|
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Perangkat Anda yang lain sudah masuk"</string>
|
||||||
<string name="screen_qr_code_login_error_expired_subtitle">"Masa masuk kedaluwarsa. Silakan coba lagi."</string>
|
<string name="screen_qr_code_login_error_expired_subtitle">"Masa masuk kedaluwarsa. Silakan coba lagi."</string>
|
||||||
<string name="screen_qr_code_login_error_expired_title">"Proses masuk tidak selesai tepat waktu"</string>
|
<string name="screen_qr_code_login_error_expired_title">"Proses masuk tidak selesai tepat waktu"</string>
|
||||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Perangkat Anda yang lain tidak mendukung masuk ke %s dengan kode QR.
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Perangkat Anda yang lain tidak mendukung masuk ke %s dengan kode QR.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<string name="screen_change_server_subtitle">"Hva er adressen til serveren din?"</string>
|
<string name="screen_change_server_subtitle">"Hva er adressen til serveren din?"</string>
|
||||||
<string name="screen_change_server_title">"Velg din server"</string>
|
<string name="screen_change_server_title">"Velg din server"</string>
|
||||||
<string name="screen_create_account_title">"Opprett konto"</string>
|
<string name="screen_create_account_title">"Opprett konto"</string>
|
||||||
<string name="screen_login_error_deactivated_account">"Denne kontoen er deaktivert."</string>
|
<string name="screen_login_error_deactivated_account">"Denne kontoen er slettet."</string>
|
||||||
<string name="screen_login_error_invalid_credentials">"Feil brukernavn og/eller passord"</string>
|
<string name="screen_login_error_invalid_credentials">"Feil brukernavn og/eller passord"</string>
|
||||||
<string name="screen_login_error_invalid_user_id">"Dette er ikke en gyldig brukeridentifikator. Forventet format: \'@bruker:homeserver.org\'"</string>
|
<string name="screen_login_error_invalid_user_id">"Dette er ikke en gyldig brukeridentifikator. Forventet format: \'@bruker:homeserver.org\'"</string>
|
||||||
<string name="screen_login_error_refresh_tokens">"Denne serveren er konfigurert til å bruke oppdateringstokener. Disse støttes ikke når du bruker passordbasert pålogging."</string>
|
<string name="screen_login_error_refresh_tokens">"Denne serveren er konfigurert til å bruke oppdateringstokener. Disse støttes ikke når du bruker passordbasert pålogging."</string>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<string name="screen_change_server_subtitle">"Địa chỉ máy chủ của bạn là gì?"</string>
|
<string name="screen_change_server_subtitle">"Địa chỉ máy chủ của bạn là gì?"</string>
|
||||||
<string name="screen_change_server_title">"Chọn máy chủ của bạn"</string>
|
<string name="screen_change_server_title">"Chọn máy chủ của bạn"</string>
|
||||||
<string name="screen_create_account_title">"Tạo tài khoản"</string>
|
<string name="screen_create_account_title">"Tạo tài khoản"</string>
|
||||||
<string name="screen_login_error_deactivated_account">"Tài khoản này đã bị vô hiệu hóa."</string>
|
<string name="screen_login_error_deactivated_account">"Tài khoản này đã bị xóa."</string>
|
||||||
<string name="screen_login_error_invalid_credentials">"Tên người dùng và/hoặc mật khẩu không chính xác"</string>
|
<string name="screen_login_error_invalid_credentials">"Tên người dùng và/hoặc mật khẩu không chính xác"</string>
|
||||||
<string name="screen_login_error_invalid_user_id">"Đây không phải là mã nhận dạng người dùng hợp lệ. Định dạng mong đợi: ‘@user:homeserver.org’"</string>
|
<string name="screen_login_error_invalid_user_id">"Đây không phải là mã nhận dạng người dùng hợp lệ. Định dạng mong đợi: ‘@user:homeserver.org’"</string>
|
||||||
<string name="screen_login_error_refresh_tokens">"Máy chủ này được cấu hình sử dụng refresh token. Điều này không được hỗ trợ khi đăng nhập bằng mật khẩu."</string>
|
<string name="screen_login_error_refresh_tokens">"Máy chủ này được cấu hình sử dụng refresh token. Điều này không được hỗ trợ khi đăng nhập bằng mật khẩu."</string>
|
||||||
|
|||||||
+80
@@ -15,6 +15,7 @@ import io.element.android.features.login.impl.accesscontrol.DefaultAccountProvid
|
|||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
import io.element.android.features.wellknown.test.anElementWellKnown
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
@@ -23,6 +24,10 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
|||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
|
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||||
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||||
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
@@ -43,6 +48,7 @@ class ChangeServerPresenterTest {
|
|||||||
createPresenter().test {
|
createPresenter().test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||||
|
assertThat(initialState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,11 +189,81 @@ class ChangeServerPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - advisor advises prompt, dialog shown before setHomeserver runs`() = runTest {
|
||||||
|
val authenticationService = FakeMatrixAuthenticationService(
|
||||||
|
setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) },
|
||||||
|
)
|
||||||
|
createPresenter(
|
||||||
|
authenticationService = authenticationService,
|
||||||
|
enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }),
|
||||||
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true),
|
||||||
|
permissionsPresenter = FakePermissionsPresenter(),
|
||||||
|
).test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None)
|
||||||
|
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
|
||||||
|
val promptState = expectMostRecentItem()
|
||||||
|
// Dialog is shown, permission has not been requested yet, setHomeserver has not been called.
|
||||||
|
assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None)
|
||||||
|
assertThat(promptState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - dismissing the dialog aborts the submit`() = runTest {
|
||||||
|
createPresenter(
|
||||||
|
authenticationService = FakeMatrixAuthenticationService(
|
||||||
|
setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) },
|
||||||
|
),
|
||||||
|
enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }),
|
||||||
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true),
|
||||||
|
permissionsPresenter = FakePermissionsPresenter(),
|
||||||
|
).test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
|
||||||
|
val promptState = expectMostRecentItem()
|
||||||
|
assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None)
|
||||||
|
promptState.eventSink.invoke(ChangeServerEvents.DismissLocalNetworkPermission)
|
||||||
|
val dismissedState = expectMostRecentItem()
|
||||||
|
assertThat(dismissedState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None)
|
||||||
|
assertThat(dismissedState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - advisor advises prompt, permission granted resumes setHomeserver`() = runTest {
|
||||||
|
val authenticationService = FakeMatrixAuthenticationService(
|
||||||
|
setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) },
|
||||||
|
)
|
||||||
|
val permissionsPresenter = FakePermissionsPresenter()
|
||||||
|
createPresenter(
|
||||||
|
authenticationService = authenticationService,
|
||||||
|
enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }),
|
||||||
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true),
|
||||||
|
permissionsPresenter = permissionsPresenter,
|
||||||
|
).test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
|
||||||
|
val promptState = expectMostRecentItem()
|
||||||
|
assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None)
|
||||||
|
permissionsPresenter.setPermissionGranted()
|
||||||
|
// Await recompositions until the deferred setHomeserver completes.
|
||||||
|
var finalState = awaitItem()
|
||||||
|
while (finalState.changeServerAction !is AsyncData.Success) {
|
||||||
|
finalState = awaitItem()
|
||||||
|
}
|
||||||
|
assertThat(finalState.changeServerAction).isEqualTo(AsyncData.Success(Unit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createPresenter(
|
private fun createPresenter(
|
||||||
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||||
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
||||||
|
localNetworkPermissionAdvisor: FakeLocalNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
|
permissionsPresenter: FakePermissionsPresenter = FakePermissionsPresenter(),
|
||||||
) = ChangeServerPresenter(
|
) = ChangeServerPresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
accountProviderDataSource = accountProviderDataSource,
|
accountProviderDataSource = accountProviderDataSource,
|
||||||
@@ -195,5 +271,9 @@ class ChangeServerPresenterTest {
|
|||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
wellknownRetriever = wellknownRetriever,
|
wellknownRetriever = wellknownRetriever,
|
||||||
),
|
),
|
||||||
|
localNetworkPermissionGate = LocalNetworkPermissionGate(
|
||||||
|
advisor = localNetworkPermissionAdvisor,
|
||||||
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -12,8 +12,8 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.login.LoginModePresenter
|
||||||
import io.element.android.features.login.impl.screens.onboarding.createLoginHelper
|
import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
|
||||||
@@ -81,7 +81,7 @@ class ChooseAccountProviderPresenterTest {
|
|||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(
|
||||||
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
|
||||||
),
|
),
|
||||||
loginHelper = createLoginHelper(
|
loginModePresenter = createLoginModePresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -105,7 +105,7 @@ class ChooseAccountProviderPresenterTest {
|
|||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(
|
||||||
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
|
||||||
),
|
),
|
||||||
loginHelper = createLoginHelper(
|
loginModePresenter = createLoginModePresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -121,12 +121,12 @@ class ChooseAccountProviderPresenterTest {
|
|||||||
|
|
||||||
// Check an error was returned
|
// Check an error was returned
|
||||||
val submittedState = awaitItem()
|
val submittedState = awaitItem()
|
||||||
assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||||
|
|
||||||
// Assert the error is then cleared
|
// Assert the error is then cleared
|
||||||
submittedState.eventSink(ChooseAccountProviderEvents.ClearError)
|
submittedState.eventSink(ChooseAccountProviderEvents.ClearError)
|
||||||
val clearedState = awaitItem()
|
val clearedState = awaitItem()
|
||||||
assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ class ChooseAccountProviderPresenterTest {
|
|||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(
|
||||||
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
|
||||||
),
|
),
|
||||||
loginHelper = createLoginHelper(
|
loginModePresenter = createLoginModePresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -152,7 +152,7 @@ class ChooseAccountProviderPresenterTest {
|
|||||||
it.eventSink(ChooseAccountProviderEvents.Continue)
|
it.eventSink(ChooseAccountProviderEvents.Continue)
|
||||||
}
|
}
|
||||||
awaitItem().also {
|
awaitItem().also {
|
||||||
assertThat(it.loginMode.isLoading()).isTrue()
|
assertThat(it.loginModeState.loginMode.isLoading()).isTrue()
|
||||||
it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider2))
|
it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider2))
|
||||||
}
|
}
|
||||||
expectNoEvents()
|
expectNoEvents()
|
||||||
@@ -162,8 +162,8 @@ class ChooseAccountProviderPresenterTest {
|
|||||||
|
|
||||||
private fun createPresenter(
|
private fun createPresenter(
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
loginHelper: LoginHelper = createLoginHelper(),
|
loginModePresenter: LoginModePresenter = createLoginModePresenter(),
|
||||||
) = ChooseAccountProviderPresenter(
|
) = ChooseAccountProviderPresenter(
|
||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
loginHelper = loginHelper,
|
loginModePresenter = loginModePresenter,
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-2
@@ -11,6 +11,7 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider
|
|||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginMode
|
||||||
|
import io.element.android.features.login.impl.login.aLoginModeState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ class ChooseAccountProviderStateTest {
|
|||||||
fun `submitEnabled returns false when there is a selectedAccountProvider but there is an error`() {
|
fun `submitEnabled returns false when there is a selectedAccountProvider but there is an error`() {
|
||||||
val sut = aChooseAccountProviderState(
|
val sut = aChooseAccountProviderState(
|
||||||
selectedAccountProvider = anAccountProvider(),
|
selectedAccountProvider = anAccountProvider(),
|
||||||
loginMode = AsyncData.Failure(Throwable("Error")),
|
loginModeState = aLoginModeState(loginMode = AsyncData.Failure(Throwable("Error"))),
|
||||||
)
|
)
|
||||||
assertThat(sut.submitEnabled).isFalse()
|
assertThat(sut.submitEnabled).isFalse()
|
||||||
}
|
}
|
||||||
@@ -44,7 +45,7 @@ class ChooseAccountProviderStateTest {
|
|||||||
fun `submitEnabled returns false when there is a selectedAccountProvider but the result is successful`() {
|
fun `submitEnabled returns false when there is a selectedAccountProvider but the result is successful`() {
|
||||||
val sut = aChooseAccountProviderState(
|
val sut = aChooseAccountProviderState(
|
||||||
selectedAccountProvider = anAccountProvider(),
|
selectedAccountProvider = anAccountProvider(),
|
||||||
loginMode = AsyncData.Success(LoginMode.PasswordLogin),
|
loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.PasswordLogin)),
|
||||||
)
|
)
|
||||||
assertThat(sut.submitEnabled).isFalse()
|
assertThat(sut.submitEnabled).isFalse()
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -17,6 +17,7 @@ import androidx.compose.ui.test.onNodeWithText
|
|||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||||
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||||
|
import io.element.android.features.login.impl.login.aLoginModeState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
@@ -69,7 +70,7 @@ class ChooseAccountProviderViewTest : RobolectricTest() {
|
|||||||
val eventSink = EventsRecorder<ChooseAccountProviderEvents>()
|
val eventSink = EventsRecorder<ChooseAccountProviderEvents>()
|
||||||
setChooseAccountProviderView(
|
setChooseAccountProviderView(
|
||||||
state = aChooseAccountProviderState(
|
state = aChooseAccountProviderState(
|
||||||
loginMode = AsyncData.Failure(AN_EXCEPTION),
|
loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AN_EXCEPTION)),
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
+9
-9
@@ -16,8 +16,8 @@ import io.element.android.features.login.impl.classic.FakeElementClassicConnecti
|
|||||||
import io.element.android.features.login.impl.classic.ROOM_KEYS_VERSION
|
import io.element.android.features.login.impl.classic.ROOM_KEYS_VERSION
|
||||||
import io.element.android.features.login.impl.classic.anElementClassicReady
|
import io.element.android.features.login.impl.classic.anElementClassicReady
|
||||||
import io.element.android.features.login.impl.classic.anElementClassicSession
|
import io.element.android.features.login.impl.classic.anElementClassicSession
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.login.LoginModePresenter
|
||||||
import io.element.android.features.login.impl.screens.onboarding.createLoginHelper
|
import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
|
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
|
||||||
@@ -42,7 +42,7 @@ class LoginWithClassicPresenterTest {
|
|||||||
assertThat(initialState.displayName).isNull()
|
assertThat(initialState.displayName).isNull()
|
||||||
assertThat(initialState.avatar).isNull()
|
assertThat(initialState.avatar).isNull()
|
||||||
assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue()
|
assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue()
|
||||||
assertThat(initialState.loginMode.isUninitialized()).isTrue()
|
assertThat(initialState.loginModeState.loginMode.isUninitialized()).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ class LoginWithClassicPresenterTest {
|
|||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
elementClassicConnection = elementClassicConnection,
|
elementClassicConnection = elementClassicConnection,
|
||||||
loginHelper = createLoginHelper(
|
loginModePresenter = createLoginModePresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -108,7 +108,7 @@ class LoginWithClassicPresenterTest {
|
|||||||
)
|
)
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
elementClassicConnection = elementClassicConnection,
|
elementClassicConnection = elementClassicConnection,
|
||||||
loginHelper = createLoginHelper(
|
loginModePresenter = createLoginModePresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -147,7 +147,7 @@ class LoginWithClassicPresenterTest {
|
|||||||
val navigateToMissingKeyBackupResult = lambdaRecorder<Unit> { }
|
val navigateToMissingKeyBackupResult = lambdaRecorder<Unit> { }
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
elementClassicConnection = elementClassicConnection,
|
elementClassicConnection = elementClassicConnection,
|
||||||
loginHelper = createLoginHelper(
|
loginModePresenter = createLoginModePresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
),
|
),
|
||||||
navigator = FakeLoginWithClassicNavigator(
|
navigator = FakeLoginWithClassicNavigator(
|
||||||
@@ -188,7 +188,7 @@ class LoginWithClassicPresenterTest {
|
|||||||
val navigateToMissingKeyBackupResult = lambdaRecorder<Unit> { }
|
val navigateToMissingKeyBackupResult = lambdaRecorder<Unit> { }
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
elementClassicConnection = elementClassicConnection,
|
elementClassicConnection = elementClassicConnection,
|
||||||
loginHelper = createLoginHelper(
|
loginModePresenter = createLoginModePresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
),
|
),
|
||||||
navigator = FakeLoginWithClassicNavigator(
|
navigator = FakeLoginWithClassicNavigator(
|
||||||
@@ -247,14 +247,14 @@ class LoginWithClassicPresenterTest {
|
|||||||
private fun createPresenter(
|
private fun createPresenter(
|
||||||
userId: UserId = A_USER_ID,
|
userId: UserId = A_USER_ID,
|
||||||
navigator: LoginWithClassicNavigator = FakeLoginWithClassicNavigator(),
|
navigator: LoginWithClassicNavigator = FakeLoginWithClassicNavigator(),
|
||||||
loginHelper: LoginHelper = createLoginHelper(),
|
loginModePresenter: LoginModePresenter = createLoginModePresenter(),
|
||||||
elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(),
|
elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(),
|
||||||
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
||||||
isEnterpriseBuild: Boolean = false,
|
isEnterpriseBuild: Boolean = false,
|
||||||
) = LoginWithClassicPresenter(
|
) = LoginWithClassicPresenter(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
loginHelper = loginHelper,
|
loginModePresenter = loginModePresenter,
|
||||||
elementClassicConnection = elementClassicConnection,
|
elementClassicConnection = elementClassicConnection,
|
||||||
accountProviderDataSource = accountProviderDataSource,
|
accountProviderDataSource = accountProviderDataSource,
|
||||||
buildMeta = aBuildMeta(
|
buildMeta = aBuildMeta(
|
||||||
|
|||||||
+38
-38
@@ -14,7 +14,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService
|
|||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginMode
|
||||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||||
import io.element.android.features.login.impl.screens.onboarding.createLoginHelper
|
import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter
|
||||||
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
||||||
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
@@ -43,7 +43,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
assertThat(initialState.isAccountCreation).isFalse()
|
assertThat(initialState.isAccountCreation).isFalse()
|
||||||
assertThat(initialState.submitEnabled).isTrue()
|
assertThat(initialState.submitEnabled).isTrue()
|
||||||
assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
|
assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
|
||||||
assertThat(initialState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
assertThat(initialState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,11 +62,11 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.submitEnabled).isTrue()
|
assertThat(loadingState.submitEnabled).isTrue()
|
||||||
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.submitEnabled).isFalse()
|
assertThat(successState.submitEnabled).isFalse()
|
||||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||||
assertThat(successState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin)
|
assertThat(successState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,11 +85,11 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.submitEnabled).isTrue()
|
assertThat(loadingState.submitEnabled).isTrue()
|
||||||
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.submitEnabled).isFalse()
|
assertThat(successState.submitEnabled).isFalse()
|
||||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,15 +110,15 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.submitEnabled).isTrue()
|
assertThat(loadingState.submitEnabled).isTrue()
|
||||||
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.submitEnabled).isFalse()
|
assertThat(successState.submitEnabled).isFalse()
|
||||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
authenticationService.givenOAuthCancelError(AN_EXCEPTION)
|
authenticationService.givenOAuthCancelError(AN_EXCEPTION)
|
||||||
defaultOAuthActionFlow.post(OAuthAction.GoBack())
|
defaultOAuthActionFlow.post(OAuthAction.GoBack())
|
||||||
val cancelFailureState = awaitItem()
|
val cancelFailureState = awaitItem()
|
||||||
assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
assertThat(cancelFailureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,14 +139,14 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.submitEnabled).isTrue()
|
assertThat(loadingState.submitEnabled).isTrue()
|
||||||
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.submitEnabled).isFalse()
|
assertThat(successState.submitEnabled).isFalse()
|
||||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
defaultOAuthActionFlow.post(OAuthAction.GoBack())
|
defaultOAuthActionFlow.post(OAuthAction.GoBack())
|
||||||
val cancelFinalState = awaitItem()
|
val cancelFinalState = awaitItem()
|
||||||
assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
|
assertThat(cancelFinalState.loginModeState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,10 +167,10 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.submitEnabled).isTrue()
|
assertThat(loadingState.submitEnabled).isTrue()
|
||||||
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true))
|
defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true))
|
||||||
val cancelFinalState = awaitItem()
|
val cancelFinalState = awaitItem()
|
||||||
assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
|
assertThat(cancelFinalState.loginModeState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,17 +191,17 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.submitEnabled).isTrue()
|
assertThat(loadingState.submitEnabled).isTrue()
|
||||||
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.submitEnabled).isFalse()
|
assertThat(successState.submitEnabled).isFalse()
|
||||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
authenticationService.givenLoginError(AN_EXCEPTION)
|
authenticationService.givenLoginError(AN_EXCEPTION)
|
||||||
defaultOAuthActionFlow.post(OAuthAction.Success("aUrl"))
|
defaultOAuthActionFlow.post(OAuthAction.Success("aUrl"))
|
||||||
val cancelLoadingState = awaitItem()
|
val cancelLoadingState = awaitItem()
|
||||||
assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
assertThat(cancelLoadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
val cancelFailureState = awaitItem()
|
val cancelFailureState = awaitItem()
|
||||||
assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
assertThat(cancelFailureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,14 +222,14 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.submitEnabled).isTrue()
|
assertThat(loadingState.submitEnabled).isTrue()
|
||||||
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.submitEnabled).isFalse()
|
assertThat(successState.submitEnabled).isFalse()
|
||||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
defaultOidcActionFlow.post(OAuthAction.Success("aUrl"))
|
defaultOidcActionFlow.post(OAuthAction.Success("aUrl"))
|
||||||
val successSuccessState = awaitItem()
|
val successSuccessState = awaitItem()
|
||||||
assertThat(successSuccessState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
assertThat(successSuccessState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +249,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
skipItems(1) // Loading
|
skipItems(1) // Loading
|
||||||
val failureState = awaitItem()
|
val failureState = awaitItem()
|
||||||
assertThat(failureState.submitEnabled).isFalse()
|
assertThat(failureState.submitEnabled).isFalse()
|
||||||
assertThat(failureState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
assertThat(failureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,12 +273,12 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
|
|
||||||
// Check an error was returned
|
// Check an error was returned
|
||||||
val submittedState = awaitItem()
|
val submittedState = awaitItem()
|
||||||
assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||||
|
|
||||||
// Assert the error is then cleared
|
// Assert the error is then cleared
|
||||||
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
|
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||||
val clearedState = awaitItem()
|
val clearedState = awaitItem()
|
||||||
assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,11 +302,11 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
skipItems(1) // Loading
|
skipItems(1) // Loading
|
||||||
// Check an error was returned
|
// Check an error was returned
|
||||||
val submittedState = awaitItem()
|
val submittedState = awaitItem()
|
||||||
assertThat(submittedState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java)
|
assertThat(submittedState.loginModeState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java)
|
||||||
// Assert the error is then cleared
|
// Assert the error is then cleared
|
||||||
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
|
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||||
val clearedState = awaitItem()
|
val clearedState = awaitItem()
|
||||||
assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,8 +326,8 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
||||||
skipItems(1) // Loading
|
skipItems(1) // Loading
|
||||||
val submittedState = awaitItem()
|
val submittedState = awaitItem()
|
||||||
assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||||
assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,8 +349,8 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
||||||
skipItems(1) // Loading
|
skipItems(1) // Loading
|
||||||
val submittedState = awaitItem()
|
val submittedState = awaitItem()
|
||||||
assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||||
assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +372,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
||||||
skipItems(1) // Loading
|
skipItems(1) // Loading
|
||||||
val submittedState = awaitItem()
|
val submittedState = awaitItem()
|
||||||
assertThat(submittedState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl))
|
assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +385,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
) = ConfirmAccountProviderPresenter(
|
) = ConfirmAccountProviderPresenter(
|
||||||
params = params,
|
params = params,
|
||||||
accountProviderDataSource = accountProviderDataSource,
|
accountProviderDataSource = accountProviderDataSource,
|
||||||
loginHelper = createLoginHelper(
|
loginModePresenter = createLoginModePresenter(
|
||||||
authenticationService = matrixAuthenticationService,
|
authenticationService = matrixAuthenticationService,
|
||||||
oAuthActionFlow = defaultOAuthActionFlow,
|
oAuthActionFlow = defaultOAuthActionFlow,
|
||||||
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
|
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
|
||||||
|
|||||||
+18
-8
@@ -15,7 +15,8 @@ import io.element.android.features.enterprise.api.EnterpriseService
|
|||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||||
|
import io.element.android.features.login.impl.login.LoginModePresenter
|
||||||
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
||||||
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||||
@@ -33,6 +34,7 @@ import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationSer
|
|||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||||
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
||||||
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||||
@@ -255,7 +257,7 @@ class OnBoardingPresenterTest {
|
|||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(
|
||||||
isAllowedToConnectToHomeserverResult = { true },
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
),
|
),
|
||||||
loginHelper = createLoginHelper(
|
loginModePresenter = createLoginModePresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
),
|
),
|
||||||
accountProviderDataSource = accountProviderDataSource,
|
accountProviderDataSource = accountProviderDataSource,
|
||||||
@@ -271,12 +273,12 @@ class OnBoardingPresenterTest {
|
|||||||
assertThat(accountProviderDataSource.flow.first().url).isEqualTo(A_HOMESERVER_URL_2)
|
assertThat(accountProviderDataSource.flow.first().url).isEqualTo(A_HOMESERVER_URL_2)
|
||||||
// Check an error was returned
|
// Check an error was returned
|
||||||
val submittedState = awaitItem()
|
val submittedState = awaitItem()
|
||||||
assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||||
|
|
||||||
// Assert the error is then cleared
|
// Assert the error is then cleared
|
||||||
submittedState.eventSink(OnBoardingEvents.ClearError)
|
submittedState.eventSink(OnBoardingEvents.ClearError)
|
||||||
val clearedState = awaitItem()
|
val clearedState = awaitItem()
|
||||||
assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,7 +294,7 @@ private fun createPresenter(
|
|||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
||||||
rageshakeFeatureAvailability: () -> Flow<Boolean> = { flowOf(true) },
|
rageshakeFeatureAvailability: () -> Flow<Boolean> = { flowOf(true) },
|
||||||
loginHelper: LoginHelper = createLoginHelper(),
|
loginModePresenter: LoginModePresenter = createLoginModePresenter(),
|
||||||
onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null },
|
onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null },
|
||||||
sessionStore: SessionStore = InMemorySessionStore(),
|
sessionStore: SessionStore = InMemorySessionStore(),
|
||||||
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
||||||
@@ -305,18 +307,26 @@ private fun createPresenter(
|
|||||||
wellknownRetriever = wellknownRetriever,
|
wellknownRetriever = wellknownRetriever,
|
||||||
),
|
),
|
||||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||||
loginHelper = loginHelper,
|
loginModePresenter = loginModePresenter,
|
||||||
onBoardingLogoResIdProvider = onBoardingLogoResIdProvider,
|
onBoardingLogoResIdProvider = onBoardingLogoResIdProvider,
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
accountProviderDataSource = accountProviderDataSource,
|
accountProviderDataSource = accountProviderDataSource,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createLoginHelper(
|
fun createLoginModePresenter(
|
||||||
oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(),
|
oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(),
|
||||||
authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||||
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
|
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
|
||||||
): LoginHelper = LoginHelper(
|
localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor =
|
||||||
|
io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor(),
|
||||||
|
permissionsPresenterFactory: io.element.android.libraries.permissions.api.PermissionsPresenter.Factory =
|
||||||
|
io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory(),
|
||||||
|
): LoginModePresenter = LoginModePresenter(
|
||||||
oAuthActionFlow = oAuthActionFlow,
|
oAuthActionFlow = oAuthActionFlow,
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
|
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
|
||||||
|
localNetworkPermissionGate = LocalNetworkPermissionGate(
|
||||||
|
advisor = localNetworkPermissionAdvisor,
|
||||||
|
permissionsPresenterFactory = permissionsPresenterFactory,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-4
@@ -21,6 +21,7 @@ import com.google.testing.junit.testparameterinjector.KotlinTestParameters.named
|
|||||||
import com.google.testing.junit.testparameterinjector.TestParameter
|
import com.google.testing.junit.testparameterinjector.TestParameter
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.features.login.impl.login.LoginMode
|
import io.element.android.features.login.impl.login.LoginMode
|
||||||
|
import io.element.android.features.login.impl.login.aLoginModeState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
@@ -148,7 +149,7 @@ class OnboardingViewTest : RobolectricTestParameter() {
|
|||||||
setOnboardingView(
|
setOnboardingView(
|
||||||
state = anOnBoardingState(
|
state = anOnBoardingState(
|
||||||
defaultAccountProvider = "element.io",
|
defaultAccountProvider = "element.io",
|
||||||
loginMode = AsyncData.Failure(AN_EXCEPTION),
|
loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AN_EXCEPTION)),
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -208,7 +209,7 @@ class OnboardingViewTest : RobolectricTestParameter() {
|
|||||||
ensureCalledOnce { callback ->
|
ensureCalledOnce { callback ->
|
||||||
setOnboardingView(
|
setOnboardingView(
|
||||||
state = anOnBoardingState(
|
state = anOnBoardingState(
|
||||||
loginMode = AsyncData.Success(LoginMode.PasswordLogin),
|
loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.PasswordLogin)),
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
),
|
),
|
||||||
onNeedLoginPassword = callback,
|
onNeedLoginPassword = callback,
|
||||||
@@ -224,7 +225,7 @@ class OnboardingViewTest : RobolectricTestParameter() {
|
|||||||
ensureCalledOnceWithParam(oAuthDetails) { callback ->
|
ensureCalledOnceWithParam(oAuthDetails) { callback ->
|
||||||
setOnboardingView(
|
setOnboardingView(
|
||||||
state = anOnBoardingState(
|
state = anOnBoardingState(
|
||||||
loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails)),
|
loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails))),
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
),
|
),
|
||||||
onOAuthDetails = callback,
|
onOAuthDetails = callback,
|
||||||
@@ -240,7 +241,7 @@ class OnboardingViewTest : RobolectricTestParameter() {
|
|||||||
ensureCalledOnceWithParam(oAuthDetails.url) { callback ->
|
ensureCalledOnceWithParam(oAuthDetails.url) { callback ->
|
||||||
setOnboardingView(
|
setOnboardingView(
|
||||||
state = anOnBoardingState(
|
state = anOnBoardingState(
|
||||||
loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")),
|
loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl"))),
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
),
|
),
|
||||||
onCreateAccountContinue = callback,
|
onCreateAccountContinue = callback,
|
||||||
|
|||||||
+6
-5
@@ -13,6 +13,7 @@ import io.element.android.features.login.impl.changeserver.aChangeServerState
|
|||||||
import io.element.android.features.login.impl.resolver.HomeserverResolver
|
import io.element.android.features.login.impl.resolver.HomeserverResolver
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker
|
import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker
|
||||||
|
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
@@ -30,7 +31,7 @@ class SearchAccountProviderPresenterTest {
|
|||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(true) })
|
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(true) })
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
@@ -44,7 +45,7 @@ class SearchAccountProviderPresenterTest {
|
|||||||
fun `present - error while checking login compatibility`() = runTest {
|
fun `present - error while checking login compatibility`() = runTest {
|
||||||
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.failure(IllegalStateException("Oops")) })
|
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.failure(IllegalStateException("Oops")) })
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
@@ -68,7 +69,7 @@ class SearchAccountProviderPresenterTest {
|
|||||||
fun `present - enter text no result`() = runTest {
|
fun `present - enter text no result`() = runTest {
|
||||||
val fakeWellknownRetriever = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(false) })
|
val fakeWellknownRetriever = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(false) })
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever, FakeLocalNetworkPermissionAdvisor()),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
@@ -101,7 +102,7 @@ class SearchAccountProviderPresenterTest {
|
|||||||
}
|
}
|
||||||
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult)
|
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult)
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
@@ -141,7 +142,7 @@ class SearchAccountProviderPresenterTest {
|
|||||||
}
|
}
|
||||||
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult)
|
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult)
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
<string name="screen_signout_confirmation_dialog_submit">"Fjern denne enheten"</string>
|
<string name="screen_signout_confirmation_dialog_submit">"Fjern denne enheten"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Fjern denne enheten"</string>
|
<string name="screen_signout_confirmation_dialog_title">"Fjern denne enheten"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Fjerner enheten …"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Fjerner enheten …"</string>
|
||||||
<string name="screen_signout_key_backup_disabled_subtitle">"Du er i ferd med å logge av din siste sesjon. Hvis du logger av nå, mister du tilgangen til de krypterte meldingene dine."</string>
|
<string name="screen_signout_key_backup_disabled_subtitle">"Dette er din eneste enhet. Hvis du fjerner den, trenger du en gjenopprettingsnøkkel for å bekrefte din digitale identitet og gjenopprette de krypterte chattene dine neste gang du logger på."</string>
|
||||||
<string name="screen_signout_key_backup_disabled_title">"Du er i ferd med å miste tilgangen til dine krypterte chatter"</string>
|
<string name="screen_signout_key_backup_disabled_title">"Du er i ferd med å miste tilgangen til dine krypterte chatter"</string>
|
||||||
<string name="screen_signout_key_backup_offline_subtitle">"Nøklene dine ble fortsatt sikkerhetskopiert da du koblet fra. Koble til igjen, slik at nøklene dine kan sikkerhetskopieres før du logger av."</string>
|
<string name="screen_signout_key_backup_offline_subtitle">"Nøklene dine ble fortsatt sikkerhetskopiert da du koblet fra. Koble til igjen, slik at nøklene dine kan sikkerhetskopieres før du logger av."</string>
|
||||||
<string name="screen_signout_key_backup_offline_title">"Nøklene dine blir fortsatt sikkerhetskopiert"</string>
|
<string name="screen_signout_key_backup_offline_title">"Nøklene dine blir fortsatt sikkerhetskopiert"</string>
|
||||||
<string name="screen_signout_key_backup_ongoing_subtitle">"Vennligst vent til dette er fullført før du fjerner denne enheten."</string>
|
<string name="screen_signout_key_backup_ongoing_subtitle">"Vennligst vent til dette er fullført før du fjerner denne enheten."</string>
|
||||||
<string name="screen_signout_key_backup_ongoing_title">"Nøklene dine blir fortsatt sikkerhetskopiert"</string>
|
<string name="screen_signout_key_backup_ongoing_title">"Nøklene dine blir fortsatt sikkerhetskopiert"</string>
|
||||||
<string name="screen_signout_preference_item">"Fjern denne enheten"</string>
|
<string name="screen_signout_preference_item">"Fjern denne enheten"</string>
|
||||||
<string name="screen_signout_recovery_disabled_subtitle">"Du er i ferd med å logge ut av din siste sesjon. Hvis du logger ut nå, mister du tilgangen til de krypterte meldingene dine."</string>
|
<string name="screen_signout_recovery_disabled_subtitle">"Dette er din eneste enhet. Hvis du fjerner den, trenger du en gjenopprettingsnøkkel for å bekrefte din digitale identitet og gjenopprette de krypterte chattene dine neste gang du logger på."</string>
|
||||||
<string name="screen_signout_recovery_disabled_title">"Du er i ferd med å miste tilgangen til dine krypterte chatter"</string>
|
<string name="screen_signout_recovery_disabled_title">"Du er i ferd med å miste tilgangen til dine krypterte chatter"</string>
|
||||||
<string name="screen_signout_save_recovery_key_subtitle">"Du er i ferd med å logge av din siste sesjon. Hvis du logger av nå, kan du miste tilgangen til de krypterte meldingene dine."</string>
|
<string name="screen_signout_save_recovery_key_subtitle">"Dette er din eneste enhet. Hvis du fjerner den, trenger du en gjenopprettingsnøkkel for å bekrefte din digitale identitet og gjenopprette de krypterte chattene dine neste gang du logger på."</string>
|
||||||
<string name="screen_signout_save_recovery_key_title">"Sørg for at du har tilgang til gjenopprettingsnøkkelen din før du fjerner denne enheten"</string>
|
<string name="screen_signout_save_recovery_key_title">"Sørg for at du har tilgang til gjenopprettingsnøkkelen din før du fjerner denne enheten"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,6 +16,18 @@
|
|||||||
<string name="emoji_picker_category_places">"Putovanja i mjesta"</string>
|
<string name="emoji_picker_category_places">"Putovanja i mjesta"</string>
|
||||||
<string name="emoji_picker_category_recent">"Nedavni emotikoni"</string>
|
<string name="emoji_picker_category_recent">"Nedavni emotikoni"</string>
|
||||||
<string name="emoji_picker_category_symbols">"Simboli"</string>
|
<string name="emoji_picker_category_symbols">"Simboli"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally">"Okreni sliku vodoravno"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Okrenuto vodoravno"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Izvornik"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically">"Okreni sliku okomito"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Okrenuto okomito"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Izvornik"</string>
|
||||||
|
<string name="screen_image_edition_a11y_rotate_to_the_left">"Okrenite sliku ulijevo"</string>
|
||||||
|
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||||
|
<item quantity="one">"%1$d stupanj"</item>
|
||||||
|
<item quantity="other">"%1$d stupnjevi"</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="screen_image_edition_title">"Uredi fotografiju"</string>
|
||||||
<string name="screen_media_upload_preview_caption_warning">"Opisi možda neće biti vidljivi osobama koji se služe starijim aplikacijama."</string>
|
<string name="screen_media_upload_preview_caption_warning">"Opisi možda neće biti vidljivi osobama koji se služe starijim aplikacijama."</string>
|
||||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Dodirnite za promjenu kvalitete prijenosa videozapisa"</string>
|
<string name="screen_media_upload_preview_change_video_quality_prompt">"Dodirnite za promjenu kvalitete prijenosa videozapisa"</string>
|
||||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Datoteka se nije mogla prenijeti."</string>
|
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Datoteka se nije mogla prenijeti."</string>
|
||||||
@@ -26,6 +38,7 @@
|
|||||||
<string name="screen_media_upload_preview_item_count">"Stavka %1$d od %2$d"</string>
|
<string name="screen_media_upload_preview_item_count">"Stavka %1$d od %2$d"</string>
|
||||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimiziraj kvalitetu slike"</string>
|
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimiziraj kvalitetu slike"</string>
|
||||||
<string name="screen_media_upload_preview_processing">"Obrada…"</string>
|
<string name="screen_media_upload_preview_processing">"Obrada…"</string>
|
||||||
|
<string name="screen_media_upload_preview_title">"Dodaj medij"</string>
|
||||||
<string name="screen_report_content_block_user">"Blokiraj korisnika"</string>
|
<string name="screen_report_content_block_user">"Blokiraj korisnika"</string>
|
||||||
<string name="screen_report_content_block_user_hint">"Označite ako želite sakriti sve trenutačne i buduće poruke od ovog korisnika"</string>
|
<string name="screen_report_content_block_user_hint">"Označite ako želite sakriti sve trenutačne i buduće poruke od ovog korisnika"</string>
|
||||||
<string name="screen_report_content_explanation">"Ova poruka bit će prijavljena administratoru vašeg matičnog poslužitelja. On neće moći pročitati nijednu šifriranu poruku."</string>
|
<string name="screen_report_content_explanation">"Ova poruka bit će prijavljena administratoru vašeg matičnog poslužitelja. On neće moći pročitati nijednu šifriranu poruku."</string>
|
||||||
|
|||||||
@@ -16,6 +16,11 @@
|
|||||||
<string name="emoji_picker_category_places">"Wisata & Tempat"</string>
|
<string name="emoji_picker_category_places">"Wisata & Tempat"</string>
|
||||||
<string name="emoji_picker_category_recent">"Emojis Sebelumnya"</string>
|
<string name="emoji_picker_category_recent">"Emojis Sebelumnya"</string>
|
||||||
<string name="emoji_picker_category_symbols">"Simbol"</string>
|
<string name="emoji_picker_category_symbols">"Simbol"</string>
|
||||||
|
<string name="screen_image_edition_a11y_rotate_to_the_left">"Putar gambar ke kiri"</string>
|
||||||
|
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||||
|
<item quantity="other">"%1$d derajat"</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="screen_image_edition_title">"Sunting Foto"</string>
|
||||||
<string name="screen_media_upload_preview_caption_warning">"Keterangan mungkin tidak terlihat oleh orang yang menggunakan aplikasi lama."</string>
|
<string name="screen_media_upload_preview_caption_warning">"Keterangan mungkin tidak terlihat oleh orang yang menggunakan aplikasi lama."</string>
|
||||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Ketuk untuk mengubah kualitas unggahan video"</string>
|
<string name="screen_media_upload_preview_change_video_quality_prompt">"Ketuk untuk mengubah kualitas unggahan video"</string>
|
||||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Dokumen tidak dapat diunggah."</string>
|
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Dokumen tidak dapat diunggah."</string>
|
||||||
@@ -26,6 +31,7 @@
|
|||||||
<string name="screen_media_upload_preview_item_count">"Item %1$d dari %2$d"</string>
|
<string name="screen_media_upload_preview_item_count">"Item %1$d dari %2$d"</string>
|
||||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimalkan kualitas gambar"</string>
|
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimalkan kualitas gambar"</string>
|
||||||
<string name="screen_media_upload_preview_processing">"Memproses…"</string>
|
<string name="screen_media_upload_preview_processing">"Memproses…"</string>
|
||||||
|
<string name="screen_media_upload_preview_title">"Tambahkan media"</string>
|
||||||
<string name="screen_report_content_block_user">"Blokir pengguna"</string>
|
<string name="screen_report_content_block_user">"Blokir pengguna"</string>
|
||||||
<string name="screen_report_content_block_user_hint">"Centang jika Anda ingin menyembunyikan semua pesan saat ini dan yang akan datang dari pengguna ini"</string>
|
<string name="screen_report_content_block_user_hint">"Centang jika Anda ingin menyembunyikan semua pesan saat ini dan yang akan datang dari pengguna ini"</string>
|
||||||
<string name="screen_report_content_explanation">"Pesan ini akan dilaporkan ke administrator homeserver Anda. Mereka tidak akan dapat membaca pesan terenkripsi apa pun."</string>
|
<string name="screen_report_content_explanation">"Pesan ini akan dilaporkan ke administrator homeserver Anda. Mereka tidak akan dapat membaca pesan terenkripsi apa pun."</string>
|
||||||
|
|||||||
@@ -16,6 +16,18 @@
|
|||||||
<string name="emoji_picker_category_places">"Viaggi & Luoghi"</string>
|
<string name="emoji_picker_category_places">"Viaggi & Luoghi"</string>
|
||||||
<string name="emoji_picker_category_recent">"Emoji recenti"</string>
|
<string name="emoji_picker_category_recent">"Emoji recenti"</string>
|
||||||
<string name="emoji_picker_category_symbols">"Simboli"</string>
|
<string name="emoji_picker_category_symbols">"Simboli"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally">"Capovolgi l\'immagine orizzontalmente"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Capovolto orizzontalmente"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Originale"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically">"Capovolgi l\'immagine verticalmente"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Capovolto verticalmente"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Originale"</string>
|
||||||
|
<string name="screen_image_edition_a11y_rotate_to_the_left">"Ruota l\'immagine verso sinistra"</string>
|
||||||
|
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||||
|
<item quantity="one">"%1$d grado"</item>
|
||||||
|
<item quantity="other">"%1$d gradi"</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="screen_image_edition_title">"Modifica foto"</string>
|
||||||
<string name="screen_media_upload_preview_caption_warning">"Le didascalie potrebbero non essere visibili agli utenti di app meno recenti."</string>
|
<string name="screen_media_upload_preview_caption_warning">"Le didascalie potrebbero non essere visibili agli utenti di app meno recenti."</string>
|
||||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tocca per modificare la qualità di caricamento del video"</string>
|
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tocca per modificare la qualità di caricamento del video"</string>
|
||||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Impossibile caricare il file."</string>
|
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Impossibile caricare il file."</string>
|
||||||
@@ -26,6 +38,7 @@
|
|||||||
<string name="screen_media_upload_preview_item_count">"Elemento %1$d di %2$d"</string>
|
<string name="screen_media_upload_preview_item_count">"Elemento %1$d di %2$d"</string>
|
||||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Ottimizza la qualità delle immagini"</string>
|
<string name="screen_media_upload_preview_optimize_image_quality_title">"Ottimizza la qualità delle immagini"</string>
|
||||||
<string name="screen_media_upload_preview_processing">"Elaborazione…"</string>
|
<string name="screen_media_upload_preview_processing">"Elaborazione…"</string>
|
||||||
|
<string name="screen_media_upload_preview_title">"Aggiungi contenuti multimediali"</string>
|
||||||
<string name="screen_report_content_block_user">"Blocca utente"</string>
|
<string name="screen_report_content_block_user">"Blocca utente"</string>
|
||||||
<string name="screen_report_content_block_user_hint">"Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente"</string>
|
<string name="screen_report_content_block_user_hint">"Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente"</string>
|
||||||
<string name="screen_report_content_explanation">"Questo messaggio verrà segnalato all\'amministratore dell\'homeserver. Questi non sarà in grado di leggere i messaggi cifrati."</string>
|
<string name="screen_report_content_explanation">"Questo messaggio verrà segnalato all\'amministratore dell\'homeserver. Questi non sarà in grado di leggere i messaggi cifrati."</string>
|
||||||
|
|||||||
@@ -16,6 +16,12 @@
|
|||||||
<string name="emoji_picker_category_places">"旅・場所"</string>
|
<string name="emoji_picker_category_places">"旅・場所"</string>
|
||||||
<string name="emoji_picker_category_recent">"最近使用"</string>
|
<string name="emoji_picker_category_recent">"最近使用"</string>
|
||||||
<string name="emoji_picker_category_symbols">"記号"</string>
|
<string name="emoji_picker_category_symbols">"記号"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally">"水平に反転"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"垂直に反転"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"元画像"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically">"垂直に反転"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"水平に反転"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"編集前"</string>
|
||||||
<string name="screen_image_edition_a11y_rotate_to_the_left">"画像を左に回転"</string>
|
<string name="screen_image_edition_a11y_rotate_to_the_left">"画像を左に回転"</string>
|
||||||
<plurals name="screen_image_edition_a11y_rotation_state">
|
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||||
<item quantity="other">"%1$d°"</item>
|
<item quantity="other">"%1$d°"</item>
|
||||||
|
|||||||
@@ -16,6 +16,18 @@
|
|||||||
<string name="emoji_picker_category_places">"Sayohat va Joylar"</string>
|
<string name="emoji_picker_category_places">"Sayohat va Joylar"</string>
|
||||||
<string name="emoji_picker_category_recent">"Oxirgi kulgichlar"</string>
|
<string name="emoji_picker_category_recent">"Oxirgi kulgichlar"</string>
|
||||||
<string name="emoji_picker_category_symbols">"Belgilar"</string>
|
<string name="emoji_picker_category_symbols">"Belgilar"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally">"Rasmni gorizontal ravishda aylantiring"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Gorizontal ravishda aylantirildi"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Asl nusxasi"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically">"Rasmni vertikal ravishda aylantirish"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Vertikal ravishda aylantirildi"</string>
|
||||||
|
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Asl nusxasi"</string>
|
||||||
|
<string name="screen_image_edition_a11y_rotate_to_the_left">"Rasmni chapga aylantiring"</string>
|
||||||
|
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||||
|
<item quantity="one">"%1$d daraja"</item>
|
||||||
|
<item quantity="other">"%1$d daraja"</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="screen_image_edition_title">"Suratni tahrirlash"</string>
|
||||||
<string name="screen_media_upload_preview_caption_warning">"Taglavhalar eski ilovalardan foydalanuvchilarga ko‘rinmasligi mumkin."</string>
|
<string name="screen_media_upload_preview_caption_warning">"Taglavhalar eski ilovalardan foydalanuvchilarga ko‘rinmasligi mumkin."</string>
|
||||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Video yuklash sifatini oʻzgartirish uchun bosing"</string>
|
<string name="screen_media_upload_preview_change_video_quality_prompt">"Video yuklash sifatini oʻzgartirish uchun bosing"</string>
|
||||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Faylni yuklab boʻlmadi."</string>
|
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Faylni yuklab boʻlmadi."</string>
|
||||||
@@ -26,6 +38,7 @@
|
|||||||
<string name="screen_media_upload_preview_item_count">"Element%1$d ning %2$d"</string>
|
<string name="screen_media_upload_preview_item_count">"Element%1$d ning %2$d"</string>
|
||||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Tasvir sifatini optimallashtirish"</string>
|
<string name="screen_media_upload_preview_optimize_image_quality_title">"Tasvir sifatini optimallashtirish"</string>
|
||||||
<string name="screen_media_upload_preview_processing">"Qayta ishlanmoqda…"</string>
|
<string name="screen_media_upload_preview_processing">"Qayta ishlanmoqda…"</string>
|
||||||
|
<string name="screen_media_upload_preview_title">"Media qoʻshish"</string>
|
||||||
<string name="screen_report_content_block_user">"Foydalanuvchini bloklash"</string>
|
<string name="screen_report_content_block_user">"Foydalanuvchini bloklash"</string>
|
||||||
<string name="screen_report_content_block_user_hint">"Ushbu foydalanuvchidan barcha joriy va kelajakdagi xabarlarni yashirishni xohlayotganingizni tekshiring"</string>
|
<string name="screen_report_content_block_user_hint">"Ushbu foydalanuvchidan barcha joriy va kelajakdagi xabarlarni yashirishni xohlayotganingizni tekshiring"</string>
|
||||||
<string name="screen_report_content_explanation">"Bu xabar uy serveringiz administratoriga xabar qilinadi. Ular hech qanday shifrlangan xabarlarni o\'qiy olmaydi."</string>
|
<string name="screen_report_content_explanation">"Bu xabar uy serveringiz administratoriga xabar qilinadi. Ular hech qanday shifrlangan xabarlarni o\'qiy olmaydi."</string>
|
||||||
|
|||||||
+5
-1
@@ -21,6 +21,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||||
|
import io.element.android.features.preferences.impl.userstatus.UserStatusState
|
||||||
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
|
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
@@ -57,6 +58,7 @@ class PreferencesRootPresenter(
|
|||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val sessionStore: SessionStore,
|
private val sessionStore: SessionStore,
|
||||||
private val sessionEnterpriseService: SessionEnterpriseService,
|
private val sessionEnterpriseService: SessionEnterpriseService,
|
||||||
|
private val userStatusPresenter: Presenter<UserStatusState>,
|
||||||
) : Presenter<PreferencesRootState> {
|
) : Presenter<PreferencesRootState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PreferencesRootState {
|
override fun present(): PreferencesRootState {
|
||||||
@@ -66,13 +68,14 @@ class PreferencesRootPresenter(
|
|||||||
// Force a refresh of the profile
|
// Force a refresh of the profile
|
||||||
matrixClient.getUserProfile()
|
matrixClient.getUserProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
val isMultiAccountEnabled by remember {
|
val isMultiAccountEnabled by remember {
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MultiAccount)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MultiAccount)
|
||||||
}.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
val showLinkNewDevice by remember {
|
val showLinkNewDevice by remember {
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.QrCodeLogin)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.QrCodeLogin)
|
||||||
}.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
|
val isUserStatusEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus).collectAsState(initial = false)
|
||||||
|
val userStatusState = if (isUserStatusEnabled) userStatusPresenter.present() else null
|
||||||
|
|
||||||
val otherSessions by remember {
|
val otherSessions by remember {
|
||||||
sessionStore.sessionsFlow().map { list ->
|
sessionStore.sessionsFlow().map { list ->
|
||||||
@@ -137,6 +140,7 @@ class PreferencesRootPresenter(
|
|||||||
|
|
||||||
return PreferencesRootState(
|
return PreferencesRootState(
|
||||||
myUser = matrixUser.value,
|
myUser = matrixUser.value,
|
||||||
|
userStatusState = userStatusState,
|
||||||
version = remember { versionFormatter.get() },
|
version = remember { versionFormatter.get() },
|
||||||
deviceId = matrixClient.deviceId,
|
deviceId = matrixClient.deviceId,
|
||||||
isMultiAccountEnabled = isMultiAccountEnabled,
|
isMultiAccountEnabled = isMultiAccountEnabled,
|
||||||
|
|||||||
+2
@@ -9,6 +9,7 @@
|
|||||||
package io.element.android.features.preferences.impl.root
|
package io.element.android.features.preferences.impl.root
|
||||||
|
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||||
|
import io.element.android.features.preferences.impl.userstatus.UserStatusState
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
@@ -16,6 +17,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
|
|
||||||
data class PreferencesRootState(
|
data class PreferencesRootState(
|
||||||
val myUser: MatrixUser,
|
val myUser: MatrixUser,
|
||||||
|
val userStatusState: UserStatusState?,
|
||||||
val version: String,
|
val version: String,
|
||||||
val deviceId: DeviceId?,
|
val deviceId: DeviceId?,
|
||||||
val isMultiAccountEnabled: Boolean,
|
val isMultiAccountEnabled: Boolean,
|
||||||
|
|||||||
+7
-1
@@ -11,6 +11,9 @@ package io.element.android.features.preferences.impl.root
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||||
|
import io.element.android.features.preferences.impl.userstatus.UserStatusPickerState
|
||||||
|
import io.element.android.features.preferences.impl.userstatus.UserStatusState
|
||||||
|
import io.element.android.features.preferences.impl.userstatus.aUserStatusState
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
@@ -64,8 +67,9 @@ open class PreferencesRootStateProvider : PreviewParameterProvider<PreferencesRo
|
|||||||
showDeveloperSettings = true,
|
showDeveloperSettings = true,
|
||||||
canDeactivateAccount = true,
|
canDeactivateAccount = true,
|
||||||
),
|
),
|
||||||
|
aPreferencesRootState(userStatusState = aUserStatusState(pickerState = UserStatusPickerState.ShowingPicker)),
|
||||||
// Minimal state
|
// Minimal state
|
||||||
aPreferencesRootState(),
|
aPreferencesRootState(userStatusState = null),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +90,7 @@ fun aPreferencesRootState(
|
|||||||
nbOfBlockedUsers: Int = 0,
|
nbOfBlockedUsers: Int = 0,
|
||||||
showLabsItem: Boolean = false,
|
showLabsItem: Boolean = false,
|
||||||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
||||||
|
userStatusState: UserStatusState? = aUserStatusState(),
|
||||||
snackbarMessage: SnackbarMessage? = null,
|
snackbarMessage: SnackbarMessage? = null,
|
||||||
eventSink: (PreferencesRootEvent) -> Unit = {},
|
eventSink: (PreferencesRootEvent) -> Unit = {},
|
||||||
) = PreferencesRootState(
|
) = PreferencesRootState(
|
||||||
@@ -105,6 +110,7 @@ fun aPreferencesRootState(
|
|||||||
nbOfBlockedUsers = nbOfBlockedUsers,
|
nbOfBlockedUsers = nbOfBlockedUsers,
|
||||||
showLabsItem = showLabsItem,
|
showLabsItem = showLabsItem,
|
||||||
directLogoutState = directLogoutState,
|
directLogoutState = directLogoutState,
|
||||||
|
userStatusState = userStatusState,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|||||||
+27
-3
@@ -10,6 +10,7 @@ package io.element.android.features.preferences.impl.root
|
|||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -23,6 +24,8 @@ import io.element.android.compound.theme.ElementTheme
|
|||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
import io.element.android.features.preferences.impl.user.UserPreferences
|
import io.element.android.features.preferences.impl.user.UserPreferences
|
||||||
|
import io.element.android.features.preferences.impl.userstatus.UserStatusState
|
||||||
|
import io.element.android.features.preferences.impl.userstatus.UserStatusView
|
||||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
@@ -85,10 +88,10 @@ fun PreferencesRootView(
|
|||||||
state = state,
|
state = state,
|
||||||
onAddAccountClick = onAddAccountClick,
|
onAddAccountClick = onAddAccountClick,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
}
|
||||||
// User status will be added here
|
if (state.userStatusState != null) {
|
||||||
|
UserStatusSection(state.userStatusState, showTopDivider = !state.isMultiAccountEnabled)
|
||||||
|
}
|
||||||
// 'Account' section
|
// 'Account' section
|
||||||
ManageAccountSection(
|
ManageAccountSection(
|
||||||
state = state,
|
state = state,
|
||||||
@@ -129,6 +132,27 @@ fun PreferencesRootView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColumnScope.UserStatusSection(
|
||||||
|
userStatusState: UserStatusState,
|
||||||
|
showTopDivider: Boolean,
|
||||||
|
) {
|
||||||
|
if (showTopDivider) {
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 8.dp,
|
||||||
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
UserStatusView(
|
||||||
|
state = userStatusState,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.MultiAccountSection(
|
private fun ColumnScope.MultiAccountSection(
|
||||||
state: PreferencesRootState,
|
state: PreferencesRootState,
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.preferences.impl.userstatus
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.BindingContainer
|
||||||
|
import dev.zacsweers.metro.Binds
|
||||||
|
import dev.zacsweers.metro.ContributesTo
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
|
@ContributesTo(SessionScope::class)
|
||||||
|
@BindingContainer
|
||||||
|
interface UserStatusBindingContainer {
|
||||||
|
@Binds
|
||||||
|
fun bindUserStatusPresenter(presenter: UserStatusPresenter): Presenter<UserStatusState>
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.preferences.impl.userstatus
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
|
|
||||||
|
sealed interface UserStatusEvent {
|
||||||
|
data object OpenPicker : UserStatusEvent
|
||||||
|
data object DismissPicker : UserStatusEvent
|
||||||
|
data class SetStatus(val status: UserStatus) : UserStatusEvent
|
||||||
|
data object OpenCustomInput : UserStatusEvent
|
||||||
|
data object CancelCustomInput : UserStatusEvent
|
||||||
|
data class UpdateCustomEmoji(val emoji: String) : UserStatusEvent
|
||||||
|
data object ClearStatus : UserStatusEvent
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.preferences.impl.userstatus
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.clearText
|
||||||
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
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 dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
class UserStatusPresenter(
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
|
) : Presenter<UserStatusState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): UserStatusState {
|
||||||
|
val userProfile by matrixClient.userProfile.collectAsState()
|
||||||
|
var pickerState by remember { mutableStateOf<UserStatusPickerState>(UserStatusPickerState.Hidden) }
|
||||||
|
val customTextFieldState = rememberTextFieldState()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
fun handleEvent(event: UserStatusEvent) {
|
||||||
|
when (event) {
|
||||||
|
UserStatusEvent.OpenPicker -> pickerState = UserStatusPickerState.ShowingPicker
|
||||||
|
UserStatusEvent.DismissPicker -> pickerState = UserStatusPickerState.Hidden
|
||||||
|
UserStatusEvent.OpenCustomInput -> {
|
||||||
|
val raw = userProfile.rawStatus
|
||||||
|
if (raw != null) {
|
||||||
|
customTextFieldState.setTextAndPlaceCursorAtEnd(raw.text)
|
||||||
|
} else {
|
||||||
|
customTextFieldState.clearText()
|
||||||
|
}
|
||||||
|
pickerState = UserStatusPickerState.CustomInput(
|
||||||
|
emoji = raw?.emoji ?: "😀",
|
||||||
|
textFieldState = customTextFieldState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is UserStatusEvent.SetStatus -> {
|
||||||
|
pickerState = UserStatusPickerState.Hidden
|
||||||
|
coroutineScope.launch { matrixClient.setUserStatus(event.status) }
|
||||||
|
}
|
||||||
|
UserStatusEvent.ClearStatus -> {
|
||||||
|
pickerState = UserStatusPickerState.Hidden
|
||||||
|
coroutineScope.launch { matrixClient.clearUserStatus() }
|
||||||
|
}
|
||||||
|
UserStatusEvent.CancelCustomInput -> pickerState = UserStatusPickerState.Hidden
|
||||||
|
is UserStatusEvent.UpdateCustomEmoji -> {
|
||||||
|
val current = pickerState as? UserStatusPickerState.CustomInput ?: return
|
||||||
|
pickerState = current.copy(emoji = event.emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserStatusState(
|
||||||
|
displayedStatus = userProfile.displayedStatus,
|
||||||
|
rawStatus = userProfile.rawStatus,
|
||||||
|
pickerState = pickerState,
|
||||||
|
eventSink = ::handleEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.preferences.impl.userstatus
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.features.preferences.impl.R
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
|
|
||||||
|
data class UserStatusState(
|
||||||
|
val displayedStatus: DisplayedStatus?,
|
||||||
|
val rawStatus: UserStatus?,
|
||||||
|
val pickerState: UserStatusPickerState,
|
||||||
|
val eventSink: (UserStatusEvent) -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface UserStatusPickerState {
|
||||||
|
data object Hidden : UserStatusPickerState
|
||||||
|
data object ShowingPicker : UserStatusPickerState
|
||||||
|
data class CustomInput(
|
||||||
|
val emoji: String,
|
||||||
|
val textFieldState: TextFieldState,
|
||||||
|
) : UserStatusPickerState
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PredefinedUserStatus(val emoji: String, @StringRes val labelRes: Int) {
|
||||||
|
IN_A_MEETING("💬", R.string.common_user_status_in_a_meeting),
|
||||||
|
FOCUS_TIME("💡", R.string.common_user_status_focus_time),
|
||||||
|
ON_THE_ROAD("🚙", R.string.common_user_status_on_the_road),
|
||||||
|
BE_RIGHT_BACK("☕", R.string.common_user_status_be_right_back),
|
||||||
|
AWAY("🌴", R.string.common_user_status_away),
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.preferences.impl.userstatus
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
|
|
||||||
|
internal class UserStatusStateProvider : PreviewParameterProvider<UserStatusState> {
|
||||||
|
override val values get() = sequenceOf(
|
||||||
|
aUserStatusState(),
|
||||||
|
aUserStatusState(displayedStatus = DisplayedStatus.UserSet(UserStatus("🌴", "Away"))),
|
||||||
|
aUserStatusState(displayedStatus = DisplayedStatus.InCall(callJoinedTs = 0L)),
|
||||||
|
aUserStatusState(pickerState = UserStatusPickerState.ShowingPicker),
|
||||||
|
aUserStatusState(
|
||||||
|
displayedStatus = DisplayedStatus.UserSet(UserStatus("🌴", "Away")),
|
||||||
|
rawStatus = UserStatus("🌴", "Away"),
|
||||||
|
pickerState = UserStatusPickerState.ShowingPicker,
|
||||||
|
),
|
||||||
|
aUserStatusState(pickerState = UserStatusPickerState.CustomInput(emoji = "😀", textFieldState = TextFieldState())),
|
||||||
|
aUserStatusState(pickerState = UserStatusPickerState.CustomInput(emoji = "🚀", textFieldState = TextFieldState("Working on something"))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aUserStatusState(
|
||||||
|
displayedStatus: DisplayedStatus? = null,
|
||||||
|
rawStatus: UserStatus? = null,
|
||||||
|
pickerState: UserStatusPickerState = UserStatusPickerState.Hidden,
|
||||||
|
eventSink: (UserStatusEvent) -> Unit = {},
|
||||||
|
) = UserStatusState(
|
||||||
|
displayedStatus = displayedStatus,
|
||||||
|
rawStatus = rawStatus,
|
||||||
|
pickerState = pickerState,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
+315
@@ -0,0 +1,315 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.preferences.impl.userstatus
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.text.input.InputTransformation
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.foundation.text.input.clearText
|
||||||
|
import androidx.compose.foundation.text.input.maxLength
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.SheetValue
|
||||||
|
import androidx.compose.material3.rememberBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
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.preferences.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.hide
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserStatusView(
|
||||||
|
state: UserStatusState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
when (val pickerState = state.pickerState) {
|
||||||
|
UserStatusPickerState.Hidden, UserStatusPickerState.ShowingPicker -> {
|
||||||
|
val displayedStatus = state.displayedStatus
|
||||||
|
if (displayedStatus != null) {
|
||||||
|
CurrentStatusRow(
|
||||||
|
displayedStatus = displayedStatus,
|
||||||
|
onClick = { state.eventSink(UserStatusEvent.OpenPicker) },
|
||||||
|
onClear = { state.eventSink(UserStatusEvent.ClearStatus) },
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptyStatusRow(
|
||||||
|
onClick = { state.eventSink(UserStatusEvent.OpenPicker) },
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (pickerState == UserStatusPickerState.ShowingPicker) {
|
||||||
|
UserStatusPickerBottomSheet(
|
||||||
|
currentRawStatus = state.rawStatus,
|
||||||
|
onDismiss = { state.eventSink(UserStatusEvent.DismissPicker) },
|
||||||
|
onSelectPredefinedStatus = { status -> state.eventSink(UserStatusEvent.SetStatus(status)) },
|
||||||
|
onSelectCustomStatus = { state.eventSink(UserStatusEvent.OpenCustomInput) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is UserStatusPickerState.CustomInput -> {
|
||||||
|
CustomStatusInputRow(
|
||||||
|
emoji = pickerState.emoji,
|
||||||
|
textFieldState = pickerState.textFieldState,
|
||||||
|
rawStatus = state.rawStatus,
|
||||||
|
onConfirm = {
|
||||||
|
state.eventSink(
|
||||||
|
UserStatusEvent.SetStatus(UserStatus(pickerState.emoji, pickerState.textFieldState.text.toString()))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onCancel = { state.eventSink(UserStatusEvent.CancelCustomInput) },
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyStatusRow(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_preferences_user_status_placeholder),
|
||||||
|
modifier = Modifier.padding(vertical = 16.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Reaction())),
|
||||||
|
modifier = modifier.clickable(onClick = onClick),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CurrentStatusRow(
|
||||||
|
displayedStatus: DisplayedStatus,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onClear: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val (emoji, text) = when (displayedStatus) {
|
||||||
|
is DisplayedStatus.UserSet -> displayedStatus.status.emoji to displayedStatus.status.text
|
||||||
|
is DisplayedStatus.InCall -> "🎧" to stringResource(R.string.common_user_status_on_a_call)
|
||||||
|
}
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(text = text, modifier = Modifier.padding(vertical = 16.dp)) },
|
||||||
|
leadingContent = ListItemContent.Custom { EmojiText(emoji) },
|
||||||
|
trailingContent = ListItemContent.Custom({
|
||||||
|
IconButton(
|
||||||
|
onClick = onClear,
|
||||||
|
// Remove the extra padding caused by IconButton
|
||||||
|
modifier = Modifier.offset(x = 12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.Close(),
|
||||||
|
contentDescription = stringResource(CommonStrings.action_clear),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmojiText(emoji: String) {
|
||||||
|
// Ensure the emoji doesn't scale with fontSize to match icons size
|
||||||
|
val fontSize = with(LocalDensity.current) { 16.dp.toSp() }
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.sizeIn(minWidth = 24.dp, minHeight = 24.dp),
|
||||||
|
text = emoji,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = fontSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun UserStatusPickerBottomSheet(
|
||||||
|
currentRawStatus: UserStatus?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSelectPredefinedStatus: (UserStatus) -> Unit,
|
||||||
|
onSelectCustomStatus: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val sheetState = rememberBottomSheetState(
|
||||||
|
initialValue = SheetValue.Hidden,
|
||||||
|
enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded)
|
||||||
|
)
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = sheetState,
|
||||||
|
scrollable = true,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
PredefinedUserStatus.entries.forEach { predefined ->
|
||||||
|
val label = stringResource(predefined.labelRes)
|
||||||
|
val predefinedUserStatus = UserStatus(emoji = predefined.emoji, text = label)
|
||||||
|
val isSelected = currentRawStatus == predefinedUserStatus
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(text = label) },
|
||||||
|
leadingContent = ListItemContent.Custom { EmojiText(predefined.emoji) },
|
||||||
|
trailingContent = if (isSelected) {
|
||||||
|
ListItemContent.Icon(IconSource.Vector(CompoundIcons.Check()), tintColor = ElementTheme.colors.iconAccentPrimary)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
sheetState.hide(coroutineScope) {
|
||||||
|
onSelectPredefinedStatus(predefinedUserStatus)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = stringResource(R.string.common_user_status_custom))
|
||||||
|
},
|
||||||
|
leadingContent = ListItemContent.Custom { EmojiText("✏️") },
|
||||||
|
onClick = {
|
||||||
|
sheetState.hide(coroutineScope) {
|
||||||
|
onSelectCustomStatus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val CUSTOM_STATUS_TEXT_MAX_LENGTH = 30
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CustomStatusInputRow(
|
||||||
|
emoji: String,
|
||||||
|
textFieldState: TextFieldState,
|
||||||
|
rawStatus: UserStatus?,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val hasText by remember { derivedStateOf { textFieldState.text.isNotEmpty() } }
|
||||||
|
val hasChanges by remember(emoji, rawStatus) {
|
||||||
|
derivedStateOf {
|
||||||
|
val text = textFieldState.text.toString()
|
||||||
|
text.isNotBlank() && UserStatus(emoji, text) != rawStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val saveLabel = stringResource(CommonStrings.action_save)
|
||||||
|
val cancelLabel = stringResource(CommonStrings.action_cancel)
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
TextField(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
state = textFieldState,
|
||||||
|
placeholder = stringResource(R.string.screen_preferences_user_status_custom_hint),
|
||||||
|
inputTransformation = InputTransformation.maxLength(CUSTOM_STATUS_TEXT_MAX_LENGTH),
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
onKeyboardAction = { if (hasChanges) onConfirm() },
|
||||||
|
lineLimits = TextFieldLineLimits.SingleLine,
|
||||||
|
trailingIcon = if (hasText) {
|
||||||
|
{
|
||||||
|
Box(modifier = Modifier.clickable { textFieldState.clearText() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.Close(),
|
||||||
|
contentDescription = stringResource(CommonStrings.action_cancel),
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = ListItemContent.Custom({
|
||||||
|
// Layout measures both buttons to determine the max natural width,
|
||||||
|
// then places only the active one.
|
||||||
|
Layout(
|
||||||
|
content = {
|
||||||
|
TextButton(onClick = onConfirm, text = saveLabel)
|
||||||
|
TextButton(onClick = onCancel, text = cancelLabel)
|
||||||
|
}
|
||||||
|
) { measurables, constraints ->
|
||||||
|
val placeables = measurables.map { it.measure(constraints) }
|
||||||
|
val maxWidth = placeables.maxOf { it.width }
|
||||||
|
val maxHeight = placeables.maxOf { it.height }
|
||||||
|
layout(maxWidth, maxHeight) {
|
||||||
|
val active = if (hasChanges) placeables[0] else placeables[1]
|
||||||
|
active.placeRelative(
|
||||||
|
x = (maxWidth - active.width) / 2,
|
||||||
|
y = (maxHeight - active.height) / 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
leadingContent = ListItemContent.Custom({
|
||||||
|
Surface(
|
||||||
|
shape = CircleShape,
|
||||||
|
border = BorderStroke(1.dp, ElementTheme.colors.bgSubtleSecondary),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(50.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable { },
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
EmojiText(emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun UserStatusViewPreview(@PreviewParameter(UserStatusStateProvider::class) state: UserStatusState) = ElementPreview {
|
||||||
|
UserStatusView(state = state)
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@
|
|||||||
<string name="screen_labs_header_title">"Jeste li spremni za eksperimentiranje?"</string>
|
<string name="screen_labs_header_title">"Jeste li spremni za eksperimentiranje?"</string>
|
||||||
<string name="screen_labs_title">"Laboratoriji"</string>
|
<string name="screen_labs_title">"Laboratoriji"</string>
|
||||||
<string name="screen_notification_settings_additional_settings_section_title">"Dodatne postavke"</string>
|
<string name="screen_notification_settings_additional_settings_section_title">"Dodatne postavke"</string>
|
||||||
|
<string name="screen_notification_settings_call_ringtone_label">"Ton zvona"</string>
|
||||||
<string name="screen_notification_settings_calls_label">"Audiopozivi i videopozivi"</string>
|
<string name="screen_notification_settings_calls_label">"Audiopozivi i videopozivi"</string>
|
||||||
<string name="screen_notification_settings_configuration_mismatch">"Neusklađenost konfiguracije"</string>
|
<string name="screen_notification_settings_configuration_mismatch">"Neusklađenost konfiguracije"</string>
|
||||||
<string name="screen_notification_settings_configuration_mismatch_description">"Pojednostavili smo postavke obavijesti kako bismo olakšali pronalaženje mogućnosti. Neke prilagođene postavke koje ste odabrali u prošlosti nisu ovdje prikazane, ali su i dalje aktivne.
|
<string name="screen_notification_settings_configuration_mismatch_description">"Pojednostavili smo postavke obavijesti kako bismo olakšali pronalaženje mogućnosti. Neke prilagođene postavke koje ste odabrali u prošlosti nisu ovdje prikazane, ali su i dalje aktivne.
|
||||||
@@ -77,14 +78,31 @@ Ako nastavite, neke od vaših postavki mogu se promijeniti."</string>
|
|||||||
<string name="screen_notification_settings_invite_for_me_label">"Pozivnice"</string>
|
<string name="screen_notification_settings_invite_for_me_label">"Pozivnice"</string>
|
||||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Vaš matični poslužitelj ne podržava ovu mogućnost u šifriranim sobama; možda nećete dobiti obavijesti u nekim sobama."</string>
|
<string name="screen_notification_settings_mentions_only_disclaimer">"Vaš matični poslužitelj ne podržava ovu mogućnost u šifriranim sobama; možda nećete dobiti obavijesti u nekim sobama."</string>
|
||||||
<string name="screen_notification_settings_mentions_section_title">"Spominjanja"</string>
|
<string name="screen_notification_settings_mentions_section_title">"Spominjanja"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_choose_other">"Odaberite drugi zvuk…"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_current_subtitle">"Trenutačno upotrebljavate %1$s"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_title">"Zvuk poruke"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_label">"Zvuk poruke"</string>
|
||||||
<string name="screen_notification_settings_mode_all">"Sve"</string>
|
<string name="screen_notification_settings_mode_all">"Sve"</string>
|
||||||
<string name="screen_notification_settings_mode_mentions">"Spominjanja"</string>
|
<string name="screen_notification_settings_mode_mentions">"Spominjanja"</string>
|
||||||
<string name="screen_notification_settings_notification_section_title">"Obavijesti me za"</string>
|
<string name="screen_notification_settings_notification_section_title">"Obavijesti me za"</string>
|
||||||
<string name="screen_notification_settings_room_mention_label">"Obavijesti me o sobi @soba"</string>
|
<string name="screen_notification_settings_room_mention_label">"Obavijesti me o sobi @soba"</string>
|
||||||
|
<string name="screen_notification_settings_sound_custom_fallback">"Prilagođeno"</string>
|
||||||
|
<string name="screen_notification_settings_sound_custom_sound_button_title">"Personalizirano zvono"</string>
|
||||||
|
<string name="screen_notification_settings_sound_delete_sound_error_title">"Pogreška pri brisanju datoteke"</string>
|
||||||
|
<string name="screen_notification_settings_sound_element_default">"Zadano za element"</string>
|
||||||
|
<string name="screen_notification_settings_sound_element_fade">"Blijeđenje elementa"</string>
|
||||||
|
<string name="screen_notification_settings_sound_import_sound_error_title">"Pogreška pri uvozu datoteke"</string>
|
||||||
|
<string name="screen_notification_settings_sound_preview_sound_error_title">"Problem s pregledom zvuka upozorenja"</string>
|
||||||
|
<string name="screen_notification_settings_sound_section_title">"Zvuk"</string>
|
||||||
|
<string name="screen_notification_settings_sound_set_sound_error_dismiss_a11y">"Pogreška zvuka odbacivanja upozorenja"</string>
|
||||||
|
<string name="screen_notification_settings_sound_set_sound_error_title">"Problem s postavljanjem zvuka upozorenja"</string>
|
||||||
|
<string name="screen_notification_settings_sound_silent">"Stišano"</string>
|
||||||
|
<string name="screen_notification_settings_sound_system_default">"Zadano sustavom"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required">"Kako biste primali obavijesti, promijenite %1$s."</string>
|
<string name="screen_notification_settings_system_notifications_action_required">"Kako biste primali obavijesti, promijenite %1$s."</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"postavke sustava"</string>
|
<string name="screen_notification_settings_system_notifications_action_required_content_link">"postavke sustava"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_turned_off">"Obavijesti sustava su isključene"</string>
|
<string name="screen_notification_settings_system_notifications_turned_off">"Obavijesti sustava su isključene"</string>
|
||||||
<string name="screen_notification_settings_title">"Obavijesti"</string>
|
<string name="screen_notification_settings_title">"Obavijesti"</string>
|
||||||
|
<string name="theme_black">"Crna"</string>
|
||||||
<string name="theme_dark">"Tamno"</string>
|
<string name="theme_dark">"Tamno"</string>
|
||||||
<string name="theme_light">"Svijetlo"</string>
|
<string name="theme_light">"Svijetlo"</string>
|
||||||
<string name="theme_system">"Sustav"</string>
|
<string name="theme_system">"Sustav"</string>
|
||||||
|
|||||||
@@ -10,6 +10,14 @@
|
|||||||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL tidak valid, pastikan Anda menyertakan protokol (http/https) dan alamat yang benar."</string>
|
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL tidak valid, pastikan Anda menyertakan protokol (http/https) dan alamat yang benar."</string>
|
||||||
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Sembunyikan avatar dalam permintaan undangan ruangan"</string>
|
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Sembunyikan avatar dalam permintaan undangan ruangan"</string>
|
||||||
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Sembunyikan pratinjau media pada lini masa"</string>
|
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Sembunyikan pratinjau media pada lini masa"</string>
|
||||||
|
<string name="screen_advanced_settings_labs">"Uji Coba"</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_description">"Jarak yang harus Anda tempuh untuk memicu pembaruan."</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_footer">"Pastikan \"Lokasi Tepat\" diaktifkan untuk aplikasi ini. Untuk mengubah izin tersebut, buka %1$s."</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_footer_link">"Pengaturan aplikasi"</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_title">"Pembaruan lokasi secara langsung"</string>
|
||||||
|
<plurals name="screen_advanced_settings_live_location_update_distance">
|
||||||
|
<item quantity="other">"Setiap %1$d meter"</item>
|
||||||
|
</plurals>
|
||||||
<string name="screen_advanced_settings_media_compression_description">"Unggah foto dan video lebih cepat dan kurangi penggunaan data"</string>
|
<string name="screen_advanced_settings_media_compression_description">"Unggah foto dan video lebih cepat dan kurangi penggunaan data"</string>
|
||||||
<string name="screen_advanced_settings_media_compression_title">"Optimalkan kualitas media"</string>
|
<string name="screen_advanced_settings_media_compression_title">"Optimalkan kualitas media"</string>
|
||||||
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderasi dan Keamanan"</string>
|
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderasi dan Keamanan"</string>
|
||||||
@@ -43,6 +51,11 @@
|
|||||||
<string name="screen_edit_profile_error_title">"Tidak dapat memperbarui profil"</string>
|
<string name="screen_edit_profile_error_title">"Tidak dapat memperbarui profil"</string>
|
||||||
<string name="screen_edit_profile_title">"Edit profil"</string>
|
<string name="screen_edit_profile_title">"Edit profil"</string>
|
||||||
<string name="screen_edit_profile_updating_details">"Memperbarui profil…"</string>
|
<string name="screen_edit_profile_updating_details">"Memperbarui profil…"</string>
|
||||||
|
<string name="screen_labs_enable_threads">"Aktifkan balasan utas"</string>
|
||||||
|
<string name="screen_labs_enable_threads_description">"Aplikasi akan dimulai ulang untuk menerapkan perubahan ini."</string>
|
||||||
|
<string name="screen_labs_header_description">"Cobalah ide-ide terbaru kami yang sedang dikembangkan. Fitur-fitur ini belum final; mungkin masih tidak stabil dan dapat berubah."</string>
|
||||||
|
<string name="screen_labs_header_title">"Merasa ingin bereksperimen?"</string>
|
||||||
|
<string name="screen_labs_title">"Uji Coba"</string>
|
||||||
<string name="screen_notification_settings_additional_settings_section_title">"Pengaturan tambahan"</string>
|
<string name="screen_notification_settings_additional_settings_section_title">"Pengaturan tambahan"</string>
|
||||||
<string name="screen_notification_settings_calls_label">"Panggilan audio dan video"</string>
|
<string name="screen_notification_settings_calls_label">"Panggilan audio dan video"</string>
|
||||||
<string name="screen_notification_settings_configuration_mismatch">"Ketidakcocokan pengaturan"</string>
|
<string name="screen_notification_settings_configuration_mismatch">"Ketidakcocokan pengaturan"</string>
|
||||||
@@ -68,10 +81,12 @@ Jika Anda melanjutkan, beberapa pengaturan Anda dapat berubah."</string>
|
|||||||
<string name="screen_notification_settings_mode_mentions">"Sebutan"</string>
|
<string name="screen_notification_settings_mode_mentions">"Sebutan"</string>
|
||||||
<string name="screen_notification_settings_notification_section_title">"Beri tahu saya tentang"</string>
|
<string name="screen_notification_settings_notification_section_title">"Beri tahu saya tentang"</string>
|
||||||
<string name="screen_notification_settings_room_mention_label">"Beri tahu saya pada @room"</string>
|
<string name="screen_notification_settings_room_mention_label">"Beri tahu saya pada @room"</string>
|
||||||
|
<string name="screen_notification_settings_sound_element_default">"Pengaturan Default Element"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required">"Untuk menerima pemberitahuan, silakan ubah %1$s Anda."</string>
|
<string name="screen_notification_settings_system_notifications_action_required">"Untuk menerima pemberitahuan, silakan ubah %1$s Anda."</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"pengaturan sistem"</string>
|
<string name="screen_notification_settings_system_notifications_action_required_content_link">"pengaturan sistem"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_turned_off">"Pemberitahuan sistem dimatikan"</string>
|
<string name="screen_notification_settings_system_notifications_turned_off">"Pemberitahuan sistem dimatikan"</string>
|
||||||
<string name="screen_notification_settings_title">"Notifikasi"</string>
|
<string name="screen_notification_settings_title">"Notifikasi"</string>
|
||||||
|
<string name="theme_black">"Hitam"</string>
|
||||||
<string name="theme_dark">"Gelap"</string>
|
<string name="theme_dark">"Gelap"</string>
|
||||||
<string name="theme_light">"Terang"</string>
|
<string name="theme_light">"Terang"</string>
|
||||||
<string name="theme_system">"Sistem"</string>
|
<string name="theme_system">"Sistem"</string>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Qualità del caricamento video"</string>
|
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Qualità del caricamento video"</string>
|
||||||
<string name="screen_advanced_settings_push_provider_android">"Fornitore di notifiche push"</string>
|
<string name="screen_advanced_settings_push_provider_android">"Fornitore di notifiche push"</string>
|
||||||
<string name="screen_advanced_settings_rich_text_editor_description">"Disattiva l\'editor di testo avanzato per scrivere manualmente in Markdown"</string>
|
<string name="screen_advanced_settings_rich_text_editor_description">"Disattiva l\'editor di testo avanzato per scrivere manualmente in Markdown"</string>
|
||||||
<string name="screen_advanced_settings_send_read_receipts">"Conferme di visualizzazione"</string>
|
<string name="screen_advanced_settings_send_read_receipts">"Conferme di lettura"</string>
|
||||||
<string name="screen_advanced_settings_send_read_receipts_description">"Se disattivato, le tue conferme di visualizzazione non verranno inviate a nessuno. Riceverai comunque conferme di visualizzazione da altri utenti."</string>
|
<string name="screen_advanced_settings_send_read_receipts_description">"Se disattivato, le tue conferme di visualizzazione non verranno inviate a nessuno. Riceverai comunque conferme di visualizzazione da altri utenti."</string>
|
||||||
<string name="screen_advanced_settings_share_presence">"Condividi presenza online"</string>
|
<string name="screen_advanced_settings_share_presence">"Condividi presenza online"</string>
|
||||||
<string name="screen_advanced_settings_share_presence_description">"Se disattivato, non potrai né inviare né ricevere conferme di lettura o notifiche di scrittura."</string>
|
<string name="screen_advanced_settings_share_presence_description">"Se disattivato, non potrai né inviare né ricevere conferme di lettura o notifiche di scrittura."</string>
|
||||||
@@ -58,6 +58,7 @@
|
|||||||
<string name="screen_labs_header_title">"Hai voglia di sperimentare?"</string>
|
<string name="screen_labs_header_title">"Hai voglia di sperimentare?"</string>
|
||||||
<string name="screen_labs_title">"Labs"</string>
|
<string name="screen_labs_title">"Labs"</string>
|
||||||
<string name="screen_notification_settings_additional_settings_section_title">"Impostazioni aggiuntive"</string>
|
<string name="screen_notification_settings_additional_settings_section_title">"Impostazioni aggiuntive"</string>
|
||||||
|
<string name="screen_notification_settings_call_ringtone_label">"Suoneria di chiamata"</string>
|
||||||
<string name="screen_notification_settings_calls_label">"Chiamate audio e video"</string>
|
<string name="screen_notification_settings_calls_label">"Chiamate audio e video"</string>
|
||||||
<string name="screen_notification_settings_configuration_mismatch">"Mancata corrispondenza di configurazione"</string>
|
<string name="screen_notification_settings_configuration_mismatch">"Mancata corrispondenza di configurazione"</string>
|
||||||
<string name="screen_notification_settings_configuration_mismatch_description">"Abbiamo semplificato le impostazioni di notifica per rendere le opzioni più facili da trovare. Alcune impostazioni personalizzate che hai scelto in passato non sono mostrate qui, ma sono ancora attive.
|
<string name="screen_notification_settings_configuration_mismatch_description">"Abbiamo semplificato le impostazioni di notifica per rendere le opzioni più facili da trovare. Alcune impostazioni personalizzate che hai scelto in passato non sono mostrate qui, ma sono ancora attive.
|
||||||
@@ -76,14 +77,31 @@ Se procedi, alcune delle tue impostazioni potrebbero cambiare."</string>
|
|||||||
<string name="screen_notification_settings_invite_for_me_label">"Inviti"</string>
|
<string name="screen_notification_settings_invite_for_me_label">"Inviti"</string>
|
||||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Il tuo homeserver non supporta questa opzione nelle stanze crifrate, quindi potresti non ricevere notifiche in alcune stanze."</string>
|
<string name="screen_notification_settings_mentions_only_disclaimer">"Il tuo homeserver non supporta questa opzione nelle stanze crifrate, quindi potresti non ricevere notifiche in alcune stanze."</string>
|
||||||
<string name="screen_notification_settings_mentions_section_title">"Menzioni"</string>
|
<string name="screen_notification_settings_mentions_section_title">"Menzioni"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_choose_other">"Scegli un altro suono…"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_current_subtitle">"Attualmente si utilizza %1$s"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_title">"Suono messaggi"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_label">"Suono messaggi"</string>
|
||||||
<string name="screen_notification_settings_mode_all">"Tutto"</string>
|
<string name="screen_notification_settings_mode_all">"Tutto"</string>
|
||||||
<string name="screen_notification_settings_mode_mentions">"Menzioni"</string>
|
<string name="screen_notification_settings_mode_mentions">"Menzioni"</string>
|
||||||
<string name="screen_notification_settings_notification_section_title">"Avvisami per"</string>
|
<string name="screen_notification_settings_notification_section_title">"Avvisami per"</string>
|
||||||
<string name="screen_notification_settings_room_mention_label">"Avvisami con @room"</string>
|
<string name="screen_notification_settings_room_mention_label">"Avvisami con @room"</string>
|
||||||
|
<string name="screen_notification_settings_sound_custom_fallback">"Personalizzato"</string>
|
||||||
|
<string name="screen_notification_settings_sound_custom_sound_button_title">"Suono personalizzato…"</string>
|
||||||
|
<string name="screen_notification_settings_sound_delete_sound_error_title">"Errore durante l\'eliminazione del file"</string>
|
||||||
|
<string name="screen_notification_settings_sound_element_default">"Predefinito di Element"</string>
|
||||||
|
<string name="screen_notification_settings_sound_element_fade">"Dissolvenza di Element"</string>
|
||||||
|
<string name="screen_notification_settings_sound_import_sound_error_title">"Errore durante l\'importazione del file"</string>
|
||||||
|
<string name="screen_notification_settings_sound_preview_sound_error_title">"Problema durante l\'anteprima del suono di avviso"</string>
|
||||||
|
<string name="screen_notification_settings_sound_section_title">"Suono"</string>
|
||||||
|
<string name="screen_notification_settings_sound_set_sound_error_dismiss_a11y">"Ignora l\'errore relativo al suono di avviso"</string>
|
||||||
|
<string name="screen_notification_settings_sound_set_sound_error_title">"Problema con l\'impostazione del suono di avviso"</string>
|
||||||
|
<string name="screen_notification_settings_sound_silent">"Silenzioso"</string>
|
||||||
|
<string name="screen_notification_settings_sound_system_default">"Predefinito"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required">"Per ricevere notifiche, modifica le tue %1$s."</string>
|
<string name="screen_notification_settings_system_notifications_action_required">"Per ricevere notifiche, modifica le tue %1$s."</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"impostazioni di sistema"</string>
|
<string name="screen_notification_settings_system_notifications_action_required_content_link">"impostazioni di sistema"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_turned_off">"Notifiche di sistema disattivate"</string>
|
<string name="screen_notification_settings_system_notifications_turned_off">"Notifiche di sistema disattivate"</string>
|
||||||
<string name="screen_notification_settings_title">"Notifiche"</string>
|
<string name="screen_notification_settings_title">"Notifiche"</string>
|
||||||
|
<string name="theme_black">"Nero"</string>
|
||||||
<string name="theme_dark">"Scuro"</string>
|
<string name="theme_dark">"Scuro"</string>
|
||||||
<string name="theme_light">"Chiaro"</string>
|
<string name="theme_light">"Chiaro"</string>
|
||||||
<string name="theme_system">"Sistema"</string>
|
<string name="theme_system">"Sistema"</string>
|
||||||
|
|||||||
@@ -11,6 +11,14 @@
|
|||||||
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Xonaga taklif so‘rovlarida avatarlarni berkitish"</string>
|
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Xonaga taklif so‘rovlarida avatarlarni berkitish"</string>
|
||||||
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Vaqt jadvalida mediaga razm solishlarni berkitish"</string>
|
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Vaqt jadvalida mediaga razm solishlarni berkitish"</string>
|
||||||
<string name="screen_advanced_settings_labs">"Laboratoriyalar"</string>
|
<string name="screen_advanced_settings_labs">"Laboratoriyalar"</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_description">"Yangilanishni ishga tushirish uchun harakat qilishingiz kerak boʻlgan masofa"</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_footer">"Ushbu ilova uchun “Aniq joylashuv” yoqilganligiga ishonch hosil qiling. Ruxsatni oʻzgartirish uchun %1$s ga oʻting."</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_footer_link">"Ilova sozlamalari"</string>
|
||||||
|
<string name="screen_advanced_settings_live_location_section_title">"Jonli joylashuv yangilanishlari"</string>
|
||||||
|
<plurals name="screen_advanced_settings_live_location_update_distance">
|
||||||
|
<item quantity="one">"Har %1$d metr"</item>
|
||||||
|
<item quantity="other">"Har %1$d metr"</item>
|
||||||
|
</plurals>
|
||||||
<string name="screen_advanced_settings_media_compression_description">"Rasm va videolarni tezroq yuklang va trafik sarfini kamaytiring"</string>
|
<string name="screen_advanced_settings_media_compression_description">"Rasm va videolarni tezroq yuklang va trafik sarfini kamaytiring"</string>
|
||||||
<string name="screen_advanced_settings_media_compression_title">"Media sifatini yaxshilash"</string>
|
<string name="screen_advanced_settings_media_compression_title">"Media sifatini yaxshilash"</string>
|
||||||
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderatsiya va xavfsizlik"</string>
|
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderatsiya va xavfsizlik"</string>
|
||||||
@@ -50,6 +58,7 @@
|
|||||||
<string name="screen_labs_header_title">"Tajribani his qilyapsizmi?"</string>
|
<string name="screen_labs_header_title">"Tajribani his qilyapsizmi?"</string>
|
||||||
<string name="screen_labs_title">"Laboratoriyalar"</string>
|
<string name="screen_labs_title">"Laboratoriyalar"</string>
|
||||||
<string name="screen_notification_settings_additional_settings_section_title">"Qo\'shimcha sozlamalar"</string>
|
<string name="screen_notification_settings_additional_settings_section_title">"Qo\'shimcha sozlamalar"</string>
|
||||||
|
<string name="screen_notification_settings_call_ringtone_label">"Qoʻngʻroq ohangi"</string>
|
||||||
<string name="screen_notification_settings_calls_label">"Audio va video qo\'ng\'iroqlar"</string>
|
<string name="screen_notification_settings_calls_label">"Audio va video qo\'ng\'iroqlar"</string>
|
||||||
<string name="screen_notification_settings_configuration_mismatch">"Konfiguratsiya mos kelmasligi"</string>
|
<string name="screen_notification_settings_configuration_mismatch">"Konfiguratsiya mos kelmasligi"</string>
|
||||||
<string name="screen_notification_settings_configuration_mismatch_description">"Variantlarni topishni osonlashtirish uchun bildirishnomalar sozlamalarini soddalashtirdik. Ilgari siz tanlagan baʼzi shaxsiy sozlamalar bu yerda koʻrsatilmaydi, lekin ular hali ham faol.
|
<string name="screen_notification_settings_configuration_mismatch_description">"Variantlarni topishni osonlashtirish uchun bildirishnomalar sozlamalarini soddalashtirdik. Ilgari siz tanlagan baʼzi shaxsiy sozlamalar bu yerda koʻrsatilmaydi, lekin ular hali ham faol.
|
||||||
@@ -68,14 +77,31 @@ Davom ettirsangiz, baʼzi sozlamalaringiz oʻzgarishi mumkin."</string>
|
|||||||
<string name="screen_notification_settings_invite_for_me_label">"Taklifnomalar"</string>
|
<string name="screen_notification_settings_invite_for_me_label">"Taklifnomalar"</string>
|
||||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Uy serveringiz shifrlangan xonalarda ushbu imkoniyatni qoʻllab-quvvatlamaydi, shuning uchun baʼzi xonalardagi xabarlarni olmasligingiz mumkin."</string>
|
<string name="screen_notification_settings_mentions_only_disclaimer">"Uy serveringiz shifrlangan xonalarda ushbu imkoniyatni qoʻllab-quvvatlamaydi, shuning uchun baʼzi xonalardagi xabarlarni olmasligingiz mumkin."</string>
|
||||||
<string name="screen_notification_settings_mentions_section_title">"Eslatmalar"</string>
|
<string name="screen_notification_settings_mentions_section_title">"Eslatmalar"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_choose_other">"Boshqa tovushni tanlang…"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_current_subtitle">"Hozir foydalanilmoqda: %1$s"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_dialog_title">"Xabar ovozi"</string>
|
||||||
|
<string name="screen_notification_settings_message_sound_label">"Xabar ovozi"</string>
|
||||||
<string name="screen_notification_settings_mode_all">"Hammasi"</string>
|
<string name="screen_notification_settings_mode_all">"Hammasi"</string>
|
||||||
<string name="screen_notification_settings_mode_mentions">"Eslatmalar"</string>
|
<string name="screen_notification_settings_mode_mentions">"Eslatmalar"</string>
|
||||||
<string name="screen_notification_settings_notification_section_title">"Menga xabar bering"</string>
|
<string name="screen_notification_settings_notification_section_title">"Menga xabar bering"</string>
|
||||||
<string name="screen_notification_settings_room_mention_label">"Menga @room orqali xabar bering"</string>
|
<string name="screen_notification_settings_room_mention_label">"Menga @room orqali xabar bering"</string>
|
||||||
|
<string name="screen_notification_settings_sound_custom_fallback">"Maxsus"</string>
|
||||||
|
<string name="screen_notification_settings_sound_custom_sound_button_title">"Maxsus ovoz…"</string>
|
||||||
|
<string name="screen_notification_settings_sound_delete_sound_error_title">"Faylni oʻchirishda xatolik yuz berdi"</string>
|
||||||
|
<string name="screen_notification_settings_sound_element_default">"Element standarti"</string>
|
||||||
|
<string name="screen_notification_settings_sound_element_fade">"Elementlarning yoʻqolishi"</string>
|
||||||
|
<string name="screen_notification_settings_sound_import_sound_error_title">"Faylni import qilishda xatolik yuz berdi"</string>
|
||||||
|
<string name="screen_notification_settings_sound_preview_sound_error_title">"Ogohlantirish ovozini oldindan koʻrishda muammo"</string>
|
||||||
|
<string name="screen_notification_settings_sound_section_title">"Ovoz"</string>
|
||||||
|
<string name="screen_notification_settings_sound_set_sound_error_dismiss_a11y">"Ogohlantirish ovozi xatosini yopish"</string>
|
||||||
|
<string name="screen_notification_settings_sound_set_sound_error_title">"Ogohlantirish ovozini sozlashda muammo"</string>
|
||||||
|
<string name="screen_notification_settings_sound_silent">"Ovozsiz"</string>
|
||||||
|
<string name="screen_notification_settings_sound_system_default">"Tizim standarti"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required">"Bildirishnomalarni olish uchun, iltimos, o\'zingizni %1$singizni o\'zgartiring."</string>
|
<string name="screen_notification_settings_system_notifications_action_required">"Bildirishnomalarni olish uchun, iltimos, o\'zingizni %1$singizni o\'zgartiring."</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"tizim sozlamalari"</string>
|
<string name="screen_notification_settings_system_notifications_action_required_content_link">"tizim sozlamalari"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_turned_off">"Tizim bildirishnomalari o\'chirilgan"</string>
|
<string name="screen_notification_settings_system_notifications_turned_off">"Tizim bildirishnomalari o\'chirilgan"</string>
|
||||||
<string name="screen_notification_settings_title">"Bildirishnomalar"</string>
|
<string name="screen_notification_settings_title">"Bildirishnomalar"</string>
|
||||||
|
<string name="theme_black">"Qora"</string>
|
||||||
<string name="theme_dark">"Tungi"</string>
|
<string name="theme_dark">"Tungi"</string>
|
||||||
<string name="theme_light">"Nur"</string>
|
<string name="theme_light">"Nur"</string>
|
||||||
<string name="theme_system">"Tizim"</string>
|
<string name="theme_system">"Tizim"</string>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ If you proceed, some of your settings may change."</string>
|
|||||||
<string name="screen_notification_settings_edit_custom_settings_section_title">"Custom setting per chat"</string>
|
<string name="screen_notification_settings_edit_custom_settings_section_title">"Custom setting per chat"</string>
|
||||||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"An error occurred while updating the notification setting."</string>
|
<string name="screen_notification_settings_edit_failed_updating_default_mode">"An error occurred while updating the notification setting."</string>
|
||||||
<string name="screen_notification_settings_edit_mode_all_messages">"All messages"</string>
|
<string name="screen_notification_settings_edit_mode_all_messages">"All messages"</string>
|
||||||
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
|
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Mentions and replies only"</string>
|
||||||
<string name="screen_notification_settings_edit_screen_direct_section_header">"On direct chats, notify me for"</string>
|
<string name="screen_notification_settings_edit_screen_direct_section_header">"On direct chats, notify me for"</string>
|
||||||
<string name="screen_notification_settings_edit_screen_group_section_header">"On group chats, notify me for"</string>
|
<string name="screen_notification_settings_edit_screen_group_section_header">"On group chats, notify me for"</string>
|
||||||
<string name="screen_notification_settings_enable_notifications">"Enable notifications on this device"</string>
|
<string name="screen_notification_settings_enable_notifications">"Enable notifications on this device"</string>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
~
|
||||||
|
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
~ Please see LICENSE files in the repository root for full details.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<string name="common_user_status_in_a_meeting">In a meeting</string>
|
||||||
|
<string name="common_user_status_focus_time">Focus time</string>
|
||||||
|
<string name="common_user_status_on_the_road">On the road</string>
|
||||||
|
<string name="common_user_status_be_right_back">Be right back</string>
|
||||||
|
<string name="common_user_status_away">Away</string>
|
||||||
|
<string name="common_user_status_on_a_call">On a call</string>
|
||||||
|
<string name="common_user_status_custom">Custom…</string>
|
||||||
|
<string name="screen_preferences_user_status_placeholder">What\'s your status?</string>
|
||||||
|
<string name="screen_preferences_user_status_custom_hint">Custom status…</string>
|
||||||
|
</resources>
|
||||||
+2
@@ -15,6 +15,7 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||||
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
|
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
|
||||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||||
|
import io.element.android.features.preferences.impl.userstatus.aUserStatusState
|
||||||
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
|
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
@@ -346,5 +347,6 @@ class PreferencesRootPresenterTest {
|
|||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
sessionEnterpriseService = sessionEnterpriseService,
|
sessionEnterpriseService = sessionEnterpriseService,
|
||||||
|
userStatusPresenter = { aUserStatusState() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -12,8 +12,11 @@ package io.element.android.features.preferences.impl.root
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.AndroidComposeUiTest
|
import androidx.compose.ui.test.AndroidComposeUiTest
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
|
import androidx.compose.ui.test.hasClickAction
|
||||||
|
import androidx.compose.ui.test.hasText
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performScrollTo
|
||||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
@@ -199,7 +202,8 @@ class PreferencesRootViewTest : RobolectricTest() {
|
|||||||
),
|
),
|
||||||
onOpenAnalytics = callback,
|
onOpenAnalytics = callback,
|
||||||
)
|
)
|
||||||
clickOn(CommonStrings.common_analytics)
|
val text = activity!!.getString(CommonStrings.common_analytics)
|
||||||
|
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +230,8 @@ class PreferencesRootViewTest : RobolectricTest() {
|
|||||||
),
|
),
|
||||||
onOpenRageShake = callback,
|
onOpenRageShake = callback,
|
||||||
)
|
)
|
||||||
clickOn(CommonStrings.common_report_a_problem)
|
val text = activity!!.getString(CommonStrings.common_report_a_problem)
|
||||||
|
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +286,8 @@ class PreferencesRootViewTest : RobolectricTest() {
|
|||||||
),
|
),
|
||||||
onOpenDeveloperSettings = callback,
|
onOpenDeveloperSettings = callback,
|
||||||
)
|
)
|
||||||
clickOn(CommonStrings.common_developer_options)
|
val text = activity!!.getString(CommonStrings.common_developer_options)
|
||||||
|
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +395,8 @@ class PreferencesRootViewTest : RobolectricTest() {
|
|||||||
),
|
),
|
||||||
onSignOutClick = callback,
|
onSignOutClick = callback,
|
||||||
)
|
)
|
||||||
clickOn(CommonStrings.action_signout)
|
val text = activity!!.getString(CommonStrings.action_signout)
|
||||||
|
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +411,8 @@ class PreferencesRootViewTest : RobolectricTest() {
|
|||||||
),
|
),
|
||||||
onDeactivateClick = callback,
|
onDeactivateClick = callback,
|
||||||
)
|
)
|
||||||
clickOn(CommonStrings.action_delete_account)
|
val text = activity!!.getString(CommonStrings.action_delete_account)
|
||||||
|
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +438,7 @@ class PreferencesRootViewTest : RobolectricTest() {
|
|||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
onNodeWithText(version).performClick()
|
onNodeWithText(version).performScrollTo().performClick()
|
||||||
eventsRecorder.assertSingle(PreferencesRootEvent.OnVersionInfoClick)
|
eventsRecorder.assertSingle(PreferencesRootEvent.OnVersionInfoClick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.preferences.impl.userstatus
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class UserStatusPresenterTest {
|
||||||
|
private fun createPresenter(client: FakeMatrixClient = FakeMatrixClient()) =
|
||||||
|
UserStatusPresenter(matrixClient = client)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state - no status set`() = runTest {
|
||||||
|
createPresenter().test {
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(state.displayedStatus).isNull()
|
||||||
|
assertThat(state.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Open event transitions to ShowingPicker`() = runTest {
|
||||||
|
createPresenter().test {
|
||||||
|
val state = awaitItem()
|
||||||
|
state.eventSink(UserStatusEvent.OpenPicker)
|
||||||
|
assertThat(awaitItem().pickerState).isEqualTo(UserStatusPickerState.ShowingPicker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Dismiss event from ShowingPicker goes back to Hidden`() = runTest {
|
||||||
|
createPresenter().test {
|
||||||
|
val state = awaitItem()
|
||||||
|
state.eventSink(UserStatusEvent.OpenPicker)
|
||||||
|
awaitItem() // ShowingPicker
|
||||||
|
state.eventSink(UserStatusEvent.DismissPicker)
|
||||||
|
assertThat(awaitItem().pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Set event calls setUserStatus and transitions to Hidden with updated status`() = runTest {
|
||||||
|
val client = FakeMatrixClient()
|
||||||
|
createPresenter(client).test {
|
||||||
|
val status = UserStatus(emoji = "💬", text = "In a meeting")
|
||||||
|
awaitItem().eventSink(UserStatusEvent.SetStatus(status))
|
||||||
|
val newState = awaitItem()
|
||||||
|
assertThat(newState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
|
assertThat(newState.displayedStatus).isEqualTo(DisplayedStatus.UserSet(status))
|
||||||
|
assertThat(client.setUserStatusCalled).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `OpenCustomInput with no existing status uses defaults`() = runTest {
|
||||||
|
createPresenter().test {
|
||||||
|
awaitItem().eventSink(UserStatusEvent.OpenCustomInput)
|
||||||
|
val state = awaitItem()
|
||||||
|
val pickerState = state.pickerState as UserStatusPickerState.CustomInput
|
||||||
|
assertThat(pickerState.emoji).isEqualTo("😀")
|
||||||
|
assertThat(pickerState.textFieldState.text.toString()).isEqualTo("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `OpenCustomInput with existing rawStatus pre-fills values`() = runTest {
|
||||||
|
val client = FakeMatrixClient()
|
||||||
|
createPresenter(client).test {
|
||||||
|
val status = UserStatus(emoji = "🌴", text = "Away")
|
||||||
|
awaitItem().eventSink(UserStatusEvent.SetStatus(status))
|
||||||
|
// Single emission: pickerState=Hidden and displayedStatus both set in one recomposition
|
||||||
|
awaitItem().eventSink(UserStatusEvent.OpenCustomInput)
|
||||||
|
val state = awaitItem()
|
||||||
|
val pickerState = state.pickerState as UserStatusPickerState.CustomInput
|
||||||
|
assertThat(pickerState.emoji).isEqualTo("🌴")
|
||||||
|
assertThat(pickerState.textFieldState.text.toString()).isEqualTo("Away")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `UpdateCustomEmoji updates emoji in CustomInput state`() = runTest {
|
||||||
|
createPresenter().test {
|
||||||
|
awaitItem().eventSink(UserStatusEvent.OpenCustomInput)
|
||||||
|
awaitItem().eventSink(UserStatusEvent.UpdateCustomEmoji("🚀"))
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat((state.pickerState as UserStatusPickerState.CustomInput).emoji).isEqualTo("🚀")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CancelCustomInput goes back to Hidden`() = runTest {
|
||||||
|
createPresenter().test {
|
||||||
|
awaitItem().eventSink(UserStatusEvent.OpenCustomInput)
|
||||||
|
awaitItem().eventSink(UserStatusEvent.CancelCustomInput) // CustomInput state, fire Cancel
|
||||||
|
assertThat(awaitItem().pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Clear event calls clearUserStatus and clears displayed status`() = runTest {
|
||||||
|
val client = FakeMatrixClient()
|
||||||
|
createPresenter(client).test {
|
||||||
|
val status = UserStatus(emoji = "☕", text = "Be right back")
|
||||||
|
awaitItem().eventSink(UserStatusEvent.SetStatus(status))
|
||||||
|
// Single emission: pickerState=Hidden and displayedStatus both set in one recomposition
|
||||||
|
awaitItem().eventSink(UserStatusEvent.ClearStatus)
|
||||||
|
val clearedState = awaitItem()
|
||||||
|
assertThat(clearedState.pickerState).isEqualTo(UserStatusPickerState.Hidden)
|
||||||
|
assertThat(clearedState.displayedStatus).isNull()
|
||||||
|
assertThat(client.clearUserStatusCalled).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="crash_detection_dialog_content">"%1$soxirgi marta ishlatilganda qulab tushdi. Biz bilan nosozlik hisobotini baham ko\'rmoqchimisiz?"</string>
|
<string name="crash_detection_dialog_content">"%1$s oxirgi marta ishlatilganda xatolik berdi. Biz bilan nosozlik hisobotini ulashmoqchimisiz?"</string>
|
||||||
<string name="rageshake_detection_dialog_content">"Siz hafsalasi pir bo\'lib telefonni silkitayotganga o\'xshaysiz. Xatolar haqida hisobot ekranini ochmoqchimisiz?"</string>
|
<string name="rageshake_detection_dialog_content">"Siz hafsalasi pir bo\'lib telefonni silkitayotganga o\'xshaysiz. Xatolar haqida hisobot ekranini ochmoqchimisiz?"</string>
|
||||||
<string name="settings_rageshake">"G\'azablanish"</string>
|
<string name="settings_rageshake">"G\'azablanish"</string>
|
||||||
<string name="settings_rageshake_detection_threshold">"Aniqlash chegarasi"</string>
|
<string name="settings_rageshake_detection_threshold">"Aniqlash chegarasi"</string>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<string name="screen_bug_report_editor_placeholder">"Opišite problem…"</string>
|
<string name="screen_bug_report_editor_placeholder">"Opišite problem…"</string>
|
||||||
<string name="screen_bug_report_editor_supporting">"Ako je moguće, molimo vas da opis bude na engleskom jeziku."</string>
|
<string name="screen_bug_report_editor_supporting">"Ako je moguće, molimo vas da opis bude na engleskom jeziku."</string>
|
||||||
<string name="screen_bug_report_error_description_too_short">"Opis je prekratak; navedite više pojedinosti o tome što se dogodilo. Hvala!"</string>
|
<string name="screen_bug_report_error_description_too_short">"Opis je prekratak; navedite više pojedinosti o tome što se dogodilo. Hvala!"</string>
|
||||||
|
<string name="screen_bug_report_github_issue_description">"Možete unijeti broj povezanog GitHub problema, ako postoji."</string>
|
||||||
|
<string name="screen_bug_report_github_issue_label">"Problem s GitHubom"</string>
|
||||||
<string name="screen_bug_report_include_crash_logs">"Pošalji zapisnike o padu aplikacije"</string>
|
<string name="screen_bug_report_include_crash_logs">"Pošalji zapisnike o padu aplikacije"</string>
|
||||||
<string name="screen_bug_report_include_logs">"Dopusti zapisnike"</string>
|
<string name="screen_bug_report_include_logs">"Dopusti zapisnike"</string>
|
||||||
<string name="screen_bug_report_include_logs_error">"Vaši su zapisnici preopširni pa ih nije moguće uključiti u ovo izvješće. Molimo vas da nam ih pošaljete na drugi način."</string>
|
<string name="screen_bug_report_include_logs_error">"Vaši su zapisnici preopširni pa ih nije moguće uključiti u ovo izvješće. Molimo vas da nam ih pošaljete na drugi način."</string>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user