Merge branch 'release/0.7.4' into main

This commit is contained in:
ganfra 2024-11-20 11:35:18 +01:00
commit e56777600a
715 changed files with 5113 additions and 3469 deletions

View file

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
variant: [debug, release, nightly, samples]
variant: [debug, release, nightly]
fail-fast: false
# Allow all jobs on develop. Just one per PR.
concurrency:
@ -82,6 +82,3 @@ jobs:
- name: Compile nightly sources
if: ${{ matrix.variant == 'nightly' }}
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Compile samples minimal
if: ${{ matrix.variant == 'samples' }}
run: ./gradlew :samples:minimal:assemble $CI_GRADLE_ARG_PROPERTIES

View file

@ -79,7 +79,7 @@ jobs:
uses: actions/download-artifact@v4
with:
name: elementx-apk-maestro
- uses: mobile-dev-inc/action-maestro-cloud@v1.9.4
- uses: mobile-dev-inc/action-maestro-cloud@v1.9.6
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}

View file

@ -82,7 +82,7 @@ jobs:
# https://github.com/codecov/codecov-action
- name: ☂️ Upload coverage reports to codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -1,3 +1,65 @@
Changes in Element X v0.7.3 (2024-11-08)
========================================
## What's Changed
### ✨ Features
* Incoming session verification by @bmarty in https://github.com/element-hq/element-x-android/pull/3733
* Remove all GPS metadata from images uploaded as media by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3781
* Send caption with image and video by @bmarty in https://github.com/element-hq/element-x-android/pull/3803
### 🙌 Improvements
* UI iteration on the encryption settings by @bmarty in https://github.com/element-hq/element-x-android/pull/3750
* Rotate firebase token in case of error by @bmarty in https://github.com/element-hq/element-x-android/pull/3755
* Optimize media upload by @bmarty in https://github.com/element-hq/element-x-android/pull/3779
* Iteration on caption by @bmarty in https://github.com/element-hq/element-x-android/pull/3816
* Hide join call button when the user is already in the call by @bmarty in https://github.com/element-hq/element-x-android/pull/3815
* Disable button during the "verifying" step. by @bmarty in https://github.com/element-hq/element-x-android/pull/3832
### 🐛 Bugfixes
* Fix oversize padding on captioned images/videos by @frebib in https://github.com/element-hq/element-x-android/pull/3732
* Fix the onboarding flow getting stuck in some cases by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3778
* bugfix: do not remove logs after sending them by @ganfra in https://github.com/element-hq/element-x-android/pull/3780
* Use in-memory thumbnail APIs when possible by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3817
* ElementCall: allow user to switch to another call. by @bmarty in https://github.com/element-hq/element-x-android/pull/3833
* Do not delete the original file if it's not a temporary file when sending it to a room. by @bmarty in https://github.com/element-hq/element-x-android/pull/3819
* Fix verification failed issue, simplify verification logic by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3830
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3798
### 🧱 Build
* Target api 35 by @bmarty in https://github.com/element-hq/element-x-android/pull/3776
### 🚧 In development 🚧
* Knocking : update create room flow by @ganfra in https://github.com/element-hq/element-x-android/pull/3804
### Dependency upgrades
* Update dependency io.nlopez.compose.rules:detekt to v0.4.17 by @renovate in https://github.com/element-hq/element-x-android/pull/3746
* Update dependency com.posthog:posthog-android to v3.8.3 - autoclosed by @renovate in https://github.com/element-hq/element-x-android/pull/3742
* Update dependency org.maplibre.gl:android-plugin-annotation-v9 to v3.0.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3702
* Update dependency com.posthog:posthog-android to v3.9.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3754
* Update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/3283
* Update camera to v1.4.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3765
* Update dependencyAnalysis to v2.4.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3773
* Update kotlin to v2.0.21-1.0.26 by @renovate in https://github.com/element-hq/element-x-android/pull/3774
* Update dependency androidx.annotation:annotation-jvm to v1.9.1 - autoclosed by @renovate in https://github.com/element-hq/element-x-android/pull/3762
* chore(deps): update dependencyanalysis to v2.4.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3791
* fix(deps): update dependency androidx.compose:compose-bom to v2024.10.01 by @renovate in https://github.com/element-hq/element-x-android/pull/3782
* Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3770
* fix(deps): update dependency androidx.constraintlayout:constraintlayout to v2.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3784
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v0.2.59 by @renovate in https://github.com/element-hq/element-x-android/pull/3809
* Update mobile-dev-inc/action-maestro-cloud action to v1.9.4 by @renovate in https://github.com/element-hq/element-x-android/pull/3820
* Update dependency com.otaliastudios:transcoder to v0.11.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3805
* Update plugin paparazzi to v1.3.5 by @renovate in https://github.com/element-hq/element-x-android/pull/3826
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.60 by @renovate in https://github.com/element-hq/element-x-android/pull/3827
### Others
* Change wording to "Verify identity" by @bmarty in https://github.com/element-hq/element-x-android/pull/3751
* Improve FakeMatrixRoom to be able to check all the parameters. by @bmarty in https://github.com/element-hq/element-x-android/pull/3761
* Editor state fixture and preview improvement by @bmarty in https://github.com/element-hq/element-x-android/pull/3758
* Enable identity pinning violation notifications unconditionally by @andybalaam in https://github.com/element-hq/element-x-android/pull/3745
* Enable predictive back gesture by @frebib in https://github.com/element-hq/element-x-android/pull/3797
* Update project status by @mxandreas in https://github.com/element-hq/element-x-android/pull/3806
* Remove code duplication - no behavior change. by @bmarty in https://github.com/element-hq/element-x-android/pull/3823
* Verification UI / UX iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/3829
## New Contributors
* @andybalaam made their first contribution in https://github.com/element-hq/element-x-android/pull/3745
* @mxandreas made their first contribution in https://github.com/element-hq/element-x-android/pull/3806
Changes in Element X v0.7.2 (2024-10-29)
========================================

View file

@ -49,8 +49,6 @@ Please ensure that you're using the project formatting rules (which are in the p
This project should compile without any special action. Just clone it and open it with Android Studio, or compile from command line using `gradlew`.
Note: please make sure that the configuration is `app` and not `samples.minimal`.
## Strings
The strings of the project are managed externally using [https://localazy.com](https://localazy.com) and shared with Element X iOS.

View file

@ -87,7 +87,7 @@ class MainActivity : NodeActivity() {
@Composable
private fun MainNodeHost() {
NodeHost(integrationPoint = appyxIntegrationPoint) {
NodeHost(integrationPoint = appyxV1IntegrationPoint) {
MainNode(
it,
plugins = listOf(

View file

@ -9,6 +9,7 @@
<locale android:name="es"/>
<locale android:name="et"/>
<locale android:name="fa"/>
<locale android:name="fi"/>
<locale android:name="fr"/>
<locale android:name="hu"/>
<locale android:name="in"/>

View file

@ -142,12 +142,12 @@ class LoggedInPresenter @Inject constructor(
.also { Timber.tag(pusherTag.value).w("No distributors available") }
.also {
// In this case, consider the push provider is chosen.
pushService.selectPushProvider(matrixClient, pushProvider)
pushService.selectPushProvider(matrixClient.sessionId, pushProvider)
}
.also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable()) }
pushService.registerWith(matrixClient, pushProvider, distributor)
} else {
val currentPushDistributor = currentPushProvider.getCurrentDistributor(matrixClient)
val currentPushDistributor = currentPushProvider.getCurrentDistributor(matrixClient.sessionId)
if (currentPushDistributor == null) {
Timber.tag(pusherTag.value).d("Register with the first available distributor")
val distributor = currentPushProvider.getDistributors().firstOrNull()

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Kirjaudu Ulos &amp; Päivitä"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Kotipalvelimesi ei enää tue vanhaa protokollaa. Kirjaudu ulos ja takaisin sisään jatkaaksesi sovelluksen käyttöä."</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Вийти та оновити"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ваш домашній сервер більше не підтримує старий протокол. Будь ласка, вийдіть і увійдіть знову, щоб продовжити використання програми."</string>
</resources>

View file

@ -378,7 +378,7 @@ class LoggedInPresenterTest {
val lambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ ->
Result.success(Unit)
}
val selectPushProviderLambda = lambdaRecorder<MatrixClient, PushProvider, Unit> { _, _ -> }
val selectPushProviderLambda = lambdaRecorder<SessionId, PushProvider, Unit> { _, _ -> }
val sessionVerificationService = FakeSessionVerificationService(
initialSessionVerifiedStatus = SessionVerifiedStatus.Verified
)
@ -408,8 +408,8 @@ class LoggedInPresenterTest {
selectPushProviderLambda.assertions()
.isCalledOnce()
.with(
// MatrixClient
any(),
// SessionId
value(A_SESSION_ID),
// PushProvider
value(pushProvider),
)
@ -481,7 +481,7 @@ class LoggedInPresenterTest {
registerWithLambda: (MatrixClient, PushProvider, Distributor) -> Result<Unit> = { _, _, _ ->
Result.success(Unit)
},
selectPushProviderLambda: (MatrixClient, PushProvider) -> Unit = { _, _ -> lambdaError() },
selectPushProviderLambda: (SessionId, PushProvider) -> Unit = { _, _ -> lambdaError() },
currentPushProvider: () -> PushProvider? = { null },
setIgnoreRegistrationErrorLambda: (SessionId, Boolean) -> Unit = { _, _ -> lambdaError() },
): PushService {

View file

@ -49,7 +49,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml"))
}
dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.4.17")
detektPlugins("io.nlopez.compose.rules:detekt:0.4.19")
}
// KtLint

View file

@ -40,7 +40,7 @@ We want:
The CI checks that:
1. The code is compiling, without any warnings, for all the app build types and variants and for the minimal app
1. The code is compiling, without any warnings, for all the app build types and variants
2. The tests are passing
3. The code quality is good (detekt, ktlint, lint)
4. The code is running and smoke tests are passing (maestro)

View file

@ -0,0 +1,2 @@
Main changes in this version: bug fixes.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Jaa anonyymejä käyttötietoja auttaaksesi meitä tunnistamaan ongelmat."</string>
<string name="screen_analytics_settings_read_terms">"Voit lukea kaikki ehtomme %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"täällä"</string>
<string name="screen_analytics_settings_share_data">"Jaa analytiikkatietoja"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Emme tallenna tai profiloi henkilötietoja"</string>
<string name="screen_analytics_prompt_help_us_improve">"Jaa anonyymejä käyttötietoja auttaaksesi meitä tunnistamaan ongelmat."</string>
<string name="screen_analytics_prompt_read_terms">"Voit lukea kaikki ehtomme %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"täällä"</string>
<string name="screen_analytics_prompt_settings">"Voit poistaa tämän käytöstä milloin tahansa"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Emme jaa tietojasi kolmansien osapuolien kanssa"</string>
<string name="screen_analytics_prompt_title">"Auta parantamaan %1$s -sovellusta"</string>
</resources>

View file

@ -5,6 +5,6 @@
<string name="screen_analytics_prompt_read_terms">"Ви можете прочитати всі наші умови %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"тут"</string>
<string name="screen_analytics_prompt_settings">"Ви можете вимкнути цю функцію в будь-який час"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Ми не передаватимемо Ваші дані третім особам"</string>
<string name="screen_analytics_prompt_title">"Допоможіть покращити %1$s"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Ми не передаватимемо ваші дані третім особам"</string>
<string name="screen_analytics_prompt_title">"Допоможіть вдосконалити %1$s"</string>
</resources>

View file

@ -8,6 +8,7 @@
package io.element.android.features.call.impl.utils
import android.annotation.SuppressLint
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationManagerCompat
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.ElementCallConfig
@ -56,11 +57,6 @@ interface ActiveCallManager {
*/
fun registerIncomingCall(notificationData: CallNotificationData)
/**
* Called when the incoming call timed out. It will remove the active call and remove any associated UI, adding a 'missed call' notification.
*/
fun incomingCallTimedOut()
/**
* Called when the active call has been hung up. It will remove any existing UI and the active call.
* @param callType The type of call that the user hung up, either an external url one or a room one.
@ -113,18 +109,24 @@ class DefaultActiveCallManager @Inject constructor(
// Wait for the ringing call to time out
delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds)
incomingCallTimedOut()
incomingCallTimedOut(displayMissedCallNotification = true)
}
}
override fun incomingCallTimedOut() {
/**
* Called when the incoming call timed out. It will remove the active call and remove any associated UI, adding a 'missed call' notification.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun incomingCallTimedOut(displayMissedCallNotification: Boolean) {
val previousActiveCall = activeCall.value ?: return
val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return
activeCall.value = null
cancelIncomingCallNotification()
displayMissedCallNotification(notificationData)
if (displayMissedCallNotification) {
displayMissedCallNotification(notificationData)
}
}
override fun hungUpCall(callType: CallType) {
@ -186,28 +188,35 @@ class DefaultActiveCallManager @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
private fun observeRingingCall() {
// This will observe ringing calls and ensure they're terminated if the room call is cancelled
// This will observe ringing calls and ensure they're terminated if the room call is cancelled or if the user
// has joined the call from another session.
activeCall
.filterNotNull()
.filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall }
.flatMapLatest { activeCall ->
val callType = activeCall.callType as CallType.RoomCall
// Get a flow of updated `hasRoomCall` values for the room
// Get a flow of updated `hasRoomCall` and `activeRoomCallParticipants` values for the room
matrixClientProvider.getOrRestore(callType.sessionId).getOrNull()
?.getRoom(callType.roomId)
?.roomInfoFlow
?.map { it.hasRoomCall }
?.map {
it.hasRoomCall to (callType.sessionId in it.activeRoomCallParticipants)
}
?: flowOf()
}
// We only want to check if the room active call status changes
.distinctUntilChanged()
// Skip the first one, we're not interested in it (if the check below passes, it had to be active anyway)
.drop(1)
.onEach { roomHasActiveCall ->
.onEach { (roomHasActiveCall, userIsInTheCall) ->
if (!roomHasActiveCall) {
// The call was cancelled
timedOutCallJob?.cancel()
incomingCallTimedOut()
incomingCallTimedOut(displayMissedCallNotification = true)
} else if (userIsInTheCall) {
// The user joined the call from another session
timedOutCallJob?.cancel()
incomingCallTimedOut(displayMissedCallNotification = false)
}
}
.launchIn(coroutineScope)

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Käynnissä oleva puhelu"</string>
<string name="call_foreground_service_message_android">"Palaa puheluun napauttamalla"</string>
<string name="call_foreground_service_title_android">"☎️ Puhelu käynnissä"</string>
<string name="screen_incoming_call_subtitle_android">"Saapuva Element Call -puhelu"</string>
</resources>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Поточний дзвінок"</string>
<string name="call_foreground_service_message_android">"Натисніть, щоб повернутися до виклику"</string>
<string name="call_foreground_service_title_android">"☎️ Триває дзвінок"</string>
<string name="call_foreground_service_channel_title_android">"Поточний виклик"</string>
<string name="call_foreground_service_message_android">"Торкніться, щоб повернутися до виклику"</string>
<string name="call_foreground_service_title_android">"☎️ Триває виклик"</string>
<string name="screen_incoming_call_subtitle_android">"Вхідний виклик Element"</string>
</resources>

View file

@ -119,7 +119,7 @@ class DefaultActiveCallManagerTest {
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
)
manager.incomingCallTimedOut()
manager.incomingCallTimedOut(displayMissedCallNotification = true)
addMissedCallNotificationLambda.assertions().isNeverCalled()
}
@ -139,7 +139,7 @@ class DefaultActiveCallManagerTest {
manager.registerIncomingCall(aCallNotificationData())
assertThat(manager.activeCall.value).isNotNull()
manager.incomingCallTimedOut()
manager.incomingCallTimedOut(displayMissedCallNotification = true)
advanceTimeBy(1)
assertThat(manager.activeCall.value).isNull()

View file

@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
class FakeActiveCallManager(
var registerIncomingCallResult: (CallNotificationData) -> Unit = {},
var incomingCallTimedOutResult: () -> Unit = {},
var hungUpCallResult: (CallType) -> Unit = {},
var joinedCallResult: (CallType) -> Unit = {},
) : ActiveCallManager {
@ -25,10 +24,6 @@ class FakeActiveCallManager(
registerIncomingCallResult(notificationData)
}
override fun incomingCallTimedOut() {
incomingCallTimedOutResult()
}
override fun hungUpCall(callType: CallType) {
hungUpCallResult(callType)
}

View file

@ -19,6 +19,4 @@ data class CreateRoomConfig(
val avatarUri: Uri? = null,
val invites: ImmutableList<MatrixUser> = persistentListOf(),
val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
) {
val isValid = roomName.isNullOrEmpty().not() && roomVisibility.isValid()
}
)

View file

@ -11,13 +11,13 @@ import android.net.Uri
import io.element.android.features.createroom.impl.configureroom.RoomAccess
import io.element.android.features.createroom.impl.configureroom.RoomAccessItem
import io.element.android.features.createroom.impl.configureroom.RoomAddress
import io.element.android.features.createroom.impl.configureroom.RoomAddressErrorState
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
import io.element.android.features.createroom.impl.di.CreateRoomScope
import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -29,6 +29,7 @@ import javax.inject.Inject
@SingleIn(CreateRoomScope::class)
class CreateRoomDataStore @Inject constructor(
val selectedUserListDataStore: UserListDataStore,
private val roomAliasHelper: RoomAliasHelper,
) {
private val createRoomConfigFlow: MutableStateFlow<CreateRoomConfig> = MutableStateFlow(CreateRoomConfig())
private var cachedAvatarUri: Uri? = null
@ -46,13 +47,13 @@ class CreateRoomDataStore @Inject constructor(
fun setRoomName(roomName: String) {
createRoomConfigFlow.getAndUpdate { config ->
/*
val newVisibility = when (config.roomVisibility) {
is RoomVisibilityState.Public -> {
val roomAddress = config.roomVisibility.roomAddress
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
config.roomVisibility.copy(
roomAddress = RoomAddress.AutoFilled(roomName),
roomAddress = RoomAddress.AutoFilled(roomAliasName),
)
} else {
config.roomVisibility
@ -60,9 +61,9 @@ class CreateRoomDataStore @Inject constructor(
}
else -> config.roomVisibility
}
*/
config.copy(
roomName = roomName.takeIf { it.isNotEmpty() },
roomVisibility = newVisibility,
)
}
}
@ -85,11 +86,13 @@ class CreateRoomDataStore @Inject constructor(
config.copy(
roomVisibility = when (visibility) {
RoomVisibilityItem.Private -> RoomVisibilityState.Private
RoomVisibilityItem.Public -> RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled(config.roomName.orEmpty()),
roomAddressErrorState = RoomAddressErrorState.None,
roomAccess = RoomAccess.Anyone,
)
RoomVisibilityItem.Public -> {
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled(roomAliasName),
roomAccess = RoomAccess.Anyone,
)
}
}
)
}

View file

@ -17,6 +17,7 @@ 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 im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore
@ -31,6 +32,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset
import io.element.android.libraries.matrix.api.createroom.RoomVisibility
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import io.element.android.libraries.matrix.api.roomAliasFromName
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
@ -39,9 +42,12 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Optional
import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
class ConfigureRoomPresenter @Inject constructor(
private val dataStore: CreateRoomDataStore,
@ -51,6 +57,7 @@ class ConfigureRoomPresenter @Inject constructor(
private val analyticsService: AnalyticsService,
permissionsPresenterFactory: PermissionsPresenter.Factory,
private val featureFlagService: FeatureFlagService,
private val roomAliasHelper: RoomAliasHelper,
) : Presenter<ConfigureRoomState> {
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
private var pendingPermissionRequest = false
@ -58,9 +65,12 @@ class ConfigureRoomPresenter @Inject constructor(
@Composable
override fun present(): ConfigureRoomState {
val cameraPermissionState = cameraPermissionPresenter.present()
val createRoomConfig = dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig())
val createRoomConfig by dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig())
val homeserverName = remember { matrixClient.userIdServerName() }
val isKnockFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(initial = false)
val roomAddressValidity = remember {
mutableStateOf<RoomAddressValidity>(RoomAddressValidity.Unknown)
}
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) },
@ -69,12 +79,12 @@ class ConfigureRoomPresenter @Inject constructor(
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri) }
)
val avatarActions by remember(createRoomConfig.value.avatarUri) {
val avatarActions by remember(createRoomConfig.avatarUri) {
derivedStateOf {
listOfNotNull(
AvatarAction.TakePhoto,
AvatarAction.ChoosePhoto,
AvatarAction.Remove.takeIf { createRoomConfig.value.avatarUri != null },
AvatarAction.Remove.takeIf { createRoomConfig.avatarUri != null },
).toImmutableList()
}
}
@ -86,6 +96,10 @@ class ConfigureRoomPresenter @Inject constructor(
}
}
RoomAddressValidityEffect(createRoomConfig.roomVisibility.roomAddress()) { newRoomAddressValidity ->
roomAddressValidity.value = newRoomAddressValidity
}
val localCoroutineScope = rememberCoroutineScope()
val createRoomAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
@ -102,7 +116,7 @@ class ConfigureRoomPresenter @Inject constructor(
is ConfigureRoomEvents.RemoveUserFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig.value)
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
is ConfigureRoomEvents.HandleAvatarAction -> {
when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
@ -122,15 +136,49 @@ class ConfigureRoomPresenter @Inject constructor(
return ConfigureRoomState(
isKnockFeatureEnabled = isKnockFeatureEnabled,
config = createRoomConfig.value,
config = createRoomConfig,
avatarActions = avatarActions,
createRoomAction = createRoomAction.value,
cameraPermissionState = cameraPermissionState,
homeserverName = homeserverName,
roomAddressValidity = roomAddressValidity.value,
eventSink = ::handleEvents,
)
}
@Composable
private fun RoomAddressValidityEffect(
roomAddress: Optional<String>,
onRoomAddressValidityChange: (RoomAddressValidity) -> Unit,
) {
val onChange by rememberUpdatedState(onRoomAddressValidityChange)
LaunchedEffect(roomAddress) {
val roomAliasName = roomAddress.getOrNull().orEmpty()
if (roomAliasName.isEmpty()) {
onChange(RoomAddressValidity.Unknown)
return@LaunchedEffect
}
// debounce the room address validation
delay(300)
val roomAlias = matrixClient.roomAliasFromName(roomAliasName).getOrNull()
if (roomAlias == null || !roomAliasHelper.isRoomAliasValid(roomAlias)) {
onChange(RoomAddressValidity.InvalidSymbols)
} else {
matrixClient.resolveRoomAlias(roomAlias)
.onSuccess { resolved ->
if (resolved.isPresent) {
onChange(RoomAddressValidity.NotAvailable)
} else {
onChange(RoomAddressValidity.Valid)
}
}
.onFailure {
onChange(RoomAddressValidity.Valid)
}
}
}
}
private fun CoroutineScope.createRoom(
config: CreateRoomConfig,
createRoomAction: MutableState<AsyncAction<RoomId>>
@ -148,7 +196,7 @@ class ConfigureRoomPresenter @Inject constructor(
preset = RoomPreset.PUBLIC_CHAT,
invite = config.invites.map { it.userId },
avatar = avatarUrl,
canonicalAlias = config.roomVisibility.roomAddress()
roomAliasName = config.roomVisibility.roomAddress()
)
} else {
CreateRoomParameters(

View file

@ -20,6 +20,10 @@ data class ConfigureRoomState(
val avatarActions: ImmutableList<AvatarAction>,
val createRoomAction: AsyncAction<RoomId>,
val cameraPermissionState: PermissionsState,
val roomAddressValidity: RoomAddressValidity,
val homeserverName: String,
val eventSink: (ConfigureRoomEvents) -> Unit
)
) {
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
(config.roomVisibility is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid)
}

View file

@ -28,9 +28,8 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
invites = aMatrixUserList().toImmutableList(),
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room 101"),
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
roomAddressErrorState = RoomAddressErrorState.None,
),
),
),
@ -40,12 +39,44 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
invites = aMatrixUserList().toImmutableList(),
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room 101"),
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
roomAddressErrorState = RoomAddressErrorState.None,
),
),
),
aConfigureRoomState(
config = CreateRoomConfig(
roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
),
),
roomAddressValidity = RoomAddressValidity.NotAvailable,
),
aConfigureRoomState(
config = CreateRoomConfig(
roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
),
),
roomAddressValidity = RoomAddressValidity.InvalidSymbols,
),
aConfigureRoomState(
config = CreateRoomConfig(
roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
),
),
roomAddressValidity = RoomAddressValidity.Valid,
),
)
}
@ -56,6 +87,7 @@ fun aConfigureRoomState(
createRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
homeserverName: String = "matrix.org",
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
eventSink: (ConfigureRoomEvents) -> Unit = { },
) = ConfigureRoomState(
config = config,
@ -64,5 +96,6 @@ fun aConfigureRoomState(
createRoomAction = createRoomAction,
cameraPermissionState = cameraPermissionState,
homeserverName = homeserverName,
roomAddressValidity = roomAddressValidity,
eventSink = eventSink,
)

View file

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
@ -23,7 +24,6 @@ import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -36,16 +36,17 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.createroom.impl.R
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
import io.element.android.libraries.designsystem.components.LabelledTextField
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -79,7 +80,7 @@ fun ConfigureRoomView(
modifier = modifier.clearFocusOnTap(focusManager),
topBar = {
ConfigureRoomToolbar(
isNextActionEnabled = state.config.isValid,
isNextActionEnabled = state.isValid,
onBackClick = onBackClick,
onNextClick = {
focusManager.clearFocus()
@ -143,8 +144,10 @@ fun ConfigureRoomView(
modifier = Modifier.padding(horizontal = 16.dp),
address = state.config.roomVisibility.roomAddress,
homeserverName = state.homeserverName,
addressValidity = state.roomAddressValidity,
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
)
Spacer(Modifier)
}
}
}
@ -217,7 +220,7 @@ private fun RoomNameWithAvatar(
modifier = Modifier.clickable(onClick = onAvatarClick),
)
LabelledTextField(
TextField(
label = stringResource(R.string.screen_create_room_room_name_label),
value = roomName,
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
@ -233,7 +236,7 @@ private fun RoomTopic(
onTopicChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
LabelledTextField(
TextField(
modifier = modifier,
label = stringResource(R.string.screen_create_room_topic_label),
value = topic,
@ -319,54 +322,56 @@ private fun RoomAccessOptions(
private fun RoomAddressField(
address: RoomAddress,
homeserverName: String,
addressValidity: RoomAddressValidity,
onAddressChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.primary,
text = stringResource(R.string.screen_create_room_room_address_section_title),
)
TextField(
modifier = Modifier.fillMaxWidth(),
value = address.value,
leadingIcon = {
Text(
text = "#",
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textSecondary,
)
},
trailingIcon = {
Text(
text = homeserverName,
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textSecondary,
modifier = Modifier.padding(end = 16.dp)
)
},
supportingText = {
Text(
text = stringResource(R.string.screen_create_room_room_address_section_footer),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
},
onValueChange = onAddressChange,
singleLine = true,
)
}
TextField(
modifier = modifier.fillMaxWidth(),
value = address.value,
label = stringResource(R.string.screen_create_room_room_address_section_title),
leadingIcon = {
Text(
text = "#",
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textSecondary,
)
},
trailingIcon = {
Text(
text = homeserverName,
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textSecondary,
)
},
supportingText = when (addressValidity) {
RoomAddressValidity.InvalidSymbols -> {
stringResource(R.string.screen_create_room_room_address_invalid_symbols_error_description)
}
RoomAddressValidity.NotAvailable -> {
stringResource(R.string.screen_create_room_room_address_not_available_error_description)
}
else -> stringResource(R.string.screen_create_room_room_address_section_footer)
},
isError = addressValidity.isError(),
onValueChange = onAddressChange,
singleLine = true,
)
}
@PreviewsDayNight
@PreviewWithLargeHeight
@Composable
internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview {
internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
ElementPreviewLight { ContentToPreview(state) }
@PreviewWithLargeHeight
@Composable
internal fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
ElementPreviewDark { ContentToPreview(state) }
@ExcludeFromCoverage
@Composable
private fun ContentToPreview(state: ConfigureRoomState) {
ConfigureRoomView(
state = state,
onBackClick = {},

View file

@ -1,17 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
/**
* Represents the error state of a room address.
*/
sealed interface RoomAddressErrorState {
data object InvalidCharacters : RoomAddressErrorState
data object AlreadyExists : RoomAddressErrorState
data object None : RoomAddressErrorState
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
import androidx.compose.runtime.Immutable
/**
* Represents the validity state of a room address.
* ie. whether it contains invalid characters, is already taken, or is valid.
*/
@Immutable
sealed interface RoomAddressValidity {
data object Unknown : RoomAddressValidity
data object InvalidSymbols : RoomAddressValidity
data object NotAvailable : RoomAddressValidity
data object Valid : RoomAddressValidity
fun isError(): Boolean {
return this is InvalidSymbols || this is NotAvailable
}
}

View file

@ -14,7 +14,6 @@ sealed interface RoomVisibilityState {
data class Public(
val roomAddress: RoomAddress,
val roomAddressErrorState: RoomAddressErrorState,
val roomAccess: RoomAccess,
) : RoomVisibilityState
@ -24,11 +23,4 @@ sealed interface RoomVisibilityState {
is Public -> Optional.of(roomAddress.value)
}
}
fun isValid(): Boolean {
return when (this) {
is Private -> true
is Public -> roomAddressErrorState is RoomAddressErrorState.None && roomAddress.value.isNotEmpty()
}
}
}

View file

@ -13,6 +13,8 @@ To můžete kdykoli změnit v nastavení místnosti."</string>
<string name="screen_create_room_room_access_section_header">"Přístup do místnosti"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Požádat o připojení"</string>
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Některé znaky nejsou povoleny. Podporovány jsou pouze písmena, číslice a následující symboly ! $ &amp; ( ) * + / ; = ? @ [ ] - . _"</string>
<string name="screen_create_room_room_address_not_available_error_description">"Tato adresa místnosti již existuje, zkuste prosím upravit pole adresy místnosti nebo změnit název místnosti"</string>
<string name="screen_create_room_room_address_section_footer">"Aby byla tato místnost viditelná v adresáři veřejných místností, budete potřebovat adresu místnosti."</string>
<string name="screen_create_room_room_address_section_title">"Adresa místnosti"</string>
<string name="screen_create_room_room_name_label">"Název místnosti"</string>

View file

@ -13,6 +13,8 @@ Sa võid seda jututoa seadistustest alati muuta."</string>
<string name="screen_create_room_room_access_section_header">"Ligipääs jututoale"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Kõik võivad paluda selle jututoaga liitumist, kuid peakasutaja või moderaator peavad selle kinnitama"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Küsi võimalust liitumiseks"</string>
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Mõned tähemärgid pole lubatud. Kasuta vaid tähti, numbreid ja neid kirjavahemärke ! $ &amp; ( ) * + / ; = ? @ [ ] - . _"</string>
<string name="screen_create_room_room_address_not_available_error_description">"Selline jututoa aadress on juba olemas. Palun proovi muuta kas aadressi või jututoa nime"</string>
<string name="screen_create_room_room_address_section_footer">"Selleks, et see jututuba oleks nähtav jututubade avalikus kataloogis, sa vajad jututoa aadressi."</string>
<string name="screen_create_room_room_address_section_title">"Jututoa aadress"</string>
<string name="screen_create_room_room_name_label">"Jututoa nimi"</string>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"Uusi huone"</string>
<string name="screen_create_room_add_people_title">"Kutsu ihmisiä"</string>
<string name="screen_create_room_error_creating_room">"Huoneen luomisessa tapahtui virhe"</string>
<string name="screen_create_room_private_option_description">"Vain kutsutut henkilöt pääsevät tähän huoneeseen. Kaikki viestit ovat päästä päähän salattuja."</string>
<string name="screen_create_room_private_option_title">"Yksityinen huone"</string>
<string name="screen_create_room_public_option_description">"Kuka tahansa voi löytää tämän huoneen.
Voit muuttaa tämän milloin tahansa huoneen asetuksista."</string>
<string name="screen_create_room_public_option_title">"Julkinen huone"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Kuka tahansa voi liittyä tähän huoneeseen"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Kuka tahansa"</string>
<string name="screen_create_room_room_access_section_header">"Huoneeseen Pääsy"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Kuka tahansa voi pyytää saada liittyä huoneeseen, mutta ylläpitäjän tai valvojan on hyväksyttävä pyyntö"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Pyydä liittymistä"</string>
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Jotkin merkit eivät ole sallittuja. Vain kirjaimet, numerot ja seuraavat symbolit ovat tuettuja ! $ &amp; ( ) * + / ; = ? @ [ ] - . _"</string>
<string name="screen_create_room_room_address_not_available_error_description">"Tämä huoneen osoite on jo käytössä, yritä muokata huoneen osoitekenttää tai muuta huoneen nimeä"</string>
<string name="screen_create_room_room_address_section_footer">"Jotta tämä huone näkyisi julkisessa huonehakemistossa, tarvitset huoneen osoitteen."</string>
<string name="screen_create_room_room_address_section_title">"Huoneen osoite"</string>
<string name="screen_create_room_room_name_label">"Huoneen nimi"</string>
<string name="screen_create_room_room_visibility_section_title">"Huoneen näkyvyys"</string>
<string name="screen_create_room_title">"Luo huone"</string>
<string name="screen_create_room_topic_label">"Aihe (valinnainen)"</string>
<string name="screen_start_chat_error_starting_chat">"Keskustelun aloituksessa tapahtui virhe"</string>
</resources>

View file

@ -13,6 +13,8 @@ Vous pouvez modifier cela à tout moment dans les paramètres du salon."</string
<string name="screen_create_room_room_access_section_header">"Accès au salon"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Tout le monde peut demander à rejoindre le salon, mais un administrateur ou un modérateur devra accepter la demande"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Demander à rejoindre"</string>
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Certains caractères ne sont pas autorisés. Seuls les lettres, les chiffres et les symboles suivants sont utilisables ! $ &amp; ( ) * + / ; = ? @ [ ] - . _"</string>
<string name="screen_create_room_room_address_not_available_error_description">"Cette adresse de salon existe déjà, veuillez essayer de modifier le champ dadresse de salon ou de modifier le nom du salon"</string>
<string name="screen_create_room_room_address_section_footer">"Pour que ce salon soit visible dans le répertoire des salons publics, vous aurez besoin dune adresse de salon."</string>
<string name="screen_create_room_room_address_section_title">"Adresse du salon"</string>
<string name="screen_create_room_room_name_label">"Nom du salon"</string>

View file

@ -13,6 +13,8 @@ Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
<string name="screen_create_room_room_access_section_header">"Akses Ruangan"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Siapa pun dapat meminta untuk bergabung dengan ruangan tetapi administrator atau moderator harus menerima permintaan tersebut"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Minta untuk bergabung"</string>
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Beberapa karakter tidak diperbolehkan. Hanya huruf, angka, dan simbol berikut didukung ! $ &amp; ( ) * + / ; = ? @ [ ] - . _"</string>
<string name="screen_create_room_room_address_not_available_error_description">"Alamat ruangan sudah ada, silakan coba sunting kolom alamat ruangan atau ubah nama 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_name_label">"Nama ruangan"</string>

View file

@ -13,6 +13,8 @@ Pode alterar esta opção nas definições da sala."</string>
<string name="screen_create_room_room_access_section_header">"Acesso à sala"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou um moderador terá de aceitar o pedido"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Pedir para participar"</string>
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Alguns caracteres não são permitidos. Apenas letras, dígitos e os seguintes símbolos são suportados! $ &amp; ( ) * + / ; = ? @ [ ] - . _"</string>
<string name="screen_create_room_room_address_not_available_error_description">"Este endereço de sala já existe, tente editar o campo de endereço da sala ou altere o nome da sala"</string>
<string name="screen_create_room_room_address_section_footer">"Para que esta sala seja visível no diretório público de salas, precisas de um endereço de sala."</string>
<string name="screen_create_room_room_address_section_title">"Endereço da sala"</string>
<string name="screen_create_room_room_name_label">"Nome da sala"</string>

View file

@ -13,6 +13,8 @@
<string name="screen_create_room_room_access_section_header">"Доступ в комнату"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Любой желающий может подать заявку на присоединение к комнате, но администратор или модератор должен будет принять запрос."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Попросить присоединиться"</string>
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Некоторые символы не допускаются. Поддерживаются только буквы, цифры и следующие символы! $ &amp; \'() * +/; =? @ [] - . _"</string>
<string name="screen_create_room_room_address_not_available_error_description">"Такой адрес комнаты уже существует, попробуйте отредактировать поле адреса комнаты или изменить название комнаты"</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_name_label">"Название комнаты"</string>

View file

@ -13,6 +13,8 @@ Môžete to kedykoľvek zmeniť v nastaveniach miestnosti."</string>
<string name="screen_create_room_room_access_section_header">"Prístup do miestnosti"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Ktokoľvek môže požiadať o pripojenie sa k miestnosti, ale administrátor alebo moderátor bude musieť žiadosť schváliť"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Požiadať o pripojenie"</string>
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Niektoré znaky nie sú povolené. Podporované sú iba písmená, číslice a nasledujúce symboly ! $ &amp; \'() * +/; =? @ [] - . _"</string>
<string name="screen_create_room_room_address_not_available_error_description">"Táto adresa miestnosti už existuje, skúste upraviť pole adresy miestnosti alebo zmeňte názov miestnosti"</string>
<string name="screen_create_room_room_address_section_footer">"Aby bola táto miestnosť viditeľná v adresári verejných miestností, budete potrebovať adresu miestnosti."</string>
<string name="screen_create_room_room_address_section_title">"Adresa miestnosti"</string>
<string name="screen_create_room_room_name_label">"Názov miestnosti"</string>

View file

@ -3,12 +3,23 @@
<string name="screen_create_room_action_create_room">"Нова кімната"</string>
<string name="screen_create_room_add_people_title">"Запросити людей"</string>
<string name="screen_create_room_error_creating_room">"Під час створення кімнати сталася помилка"</string>
<string name="screen_create_room_private_option_description">"Повідомлення в цій кімнаті зашифровані. Пізніше шифрування вимкнути не можна."</string>
<string name="screen_create_room_private_option_description">"Лише запрошені люди мають доступ до цієї кімнати. Усі повідомлення захищені наскрізним шифруванням."</string>
<string name="screen_create_room_private_option_title">"Приватна кімната (тільки за запрошенням)"</string>
<string name="screen_create_room_public_option_description">"Повідомлення не шифруються, і будь-хто може їх прочитати. Шифрування можна ввімкнути пізніше."</string>
<string name="screen_create_room_public_option_title">"Загальна кімната (будь-хто)"</string>
<string name="screen_create_room_public_option_description">"Будь-хто може знайти цю кімнату.
Ви можете змінити це в будь-який час у налаштуваннях кімнати."</string>
<string name="screen_create_room_public_option_title">"Публічна кімната"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Будь-хто може приєднатися до цієї кімнати"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Кожний"</string>
<string name="screen_create_room_room_access_section_header">"Доступ до кімнати"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Будь-хто може попросити приєднатися до кімнати, але адміністратор або модератор повинен буде прийняти запит"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Запросити приєднатися"</string>
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Деякі символи не допускаються. Підтримуються тільки букви, цифри і наступні символи! $ &amp; ( ) * + / ; = ? @ [ ] - . _"</string>
<string name="screen_create_room_room_address_not_available_error_description">"Ця адреса кімнати вже існує, будь ласка, спробуйте відредагувати поле адреси кімнати або змінити назву кімнати"</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_name_label">"Назва кімнати"</string>
<string name="screen_create_room_room_visibility_section_title">"Видимість кімнати"</string>
<string name="screen_create_room_title">"Створити кімнату"</string>
<string name="screen_create_room_topic_label">"Тема (необов\'язково)"</string>
<string name="screen_start_chat_error_starting_chat">"Під час спроби почати чат сталася помилка"</string>
<string name="screen_start_chat_error_starting_chat">"Під час спроби почати бесіду сталася помилка"</string>
</resources>

View file

@ -2,13 +2,22 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"新聊天室"</string>
<string name="screen_create_room_add_people_title">"邀请朋友"</string>
<string name="screen_create_room_error_creating_room">"创建房间时出错"</string>
<string name="screen_create_room_private_option_description">"此聊天室中的消息已加密。加密无法禁用。"</string>
<string name="screen_create_room_private_option_title">"私人房间(仅限受邀者)"</string>
<string name="screen_create_room_public_option_description">"消息未加密,任何人都可以查看。可以稍后启用加密。"</string>
<string name="screen_create_room_public_option_title">"公共房间(任何人)"</string>
<string name="screen_create_room_room_name_label">"房间名称"</string>
<string name="screen_create_room_title">"创建房间"</string>
<string name="screen_create_room_error_creating_room">"创建聊天室时出错"</string>
<string name="screen_create_room_private_option_description">"只有受邀用户才能访问此房间。所有消息均经过端到端加密。"</string>
<string name="screen_create_room_private_option_title">"私有房间"</string>
<string name="screen_create_room_public_option_description">"任何人都能找到此房间。
你可以随时在房间设置中更改。"</string>
<string name="screen_create_room_public_option_title">"公开房间"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"任何人都可以加入此房间"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"任何人"</string>
<string name="screen_create_room_room_access_section_header">"房间访问权限"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"任何人都可以请求加入房间,但必须由管理员或审核人接受"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"请求加入"</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_name_label">"聊天室名称"</string>
<string name="screen_create_room_room_visibility_section_title">"房间可见性"</string>
<string name="screen_create_room_title">"创建聊天室"</string>
<string name="screen_create_room_topic_label">"主题(可选)"</string>
<string name="screen_start_chat_error_starting_chat">"在开始聊天时发生了错误"</string>
</resources>

View file

@ -13,6 +13,8 @@ You can change this anytime in room settings."</string>
<string name="screen_create_room_room_access_section_header">"Room Access"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Anyone can ask to join the room but an administrator or a moderator will have to accept the request"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Ask to join"</string>
<string name="screen_create_room_room_address_invalid_symbols_error_description">"Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ &amp; ( ) * + / ; = ? @ [ ] - . _"</string>
<string name="screen_create_room_room_address_not_available_error_description">"This room address already exists, please try editing the room address field or change the room name"</string>
<string name="screen_create_room_room_address_section_footer">"In order for this room to be visible in the public room directory, you will need a room address."</string>
<string name="screen_create_room_room_address_section_title">"Room address"</string>
<string name="screen_create_room_room_name_label">"Room name"</string>

View file

@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
import io.element.android.libraries.usersearch.test.FakeUserRepository
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
@ -32,7 +33,7 @@ class AddPeoplePresenterTest {
presenter = AddPeoplePresenter(
FakeUserListPresenterFactory(),
FakeUserRepository(),
CreateRoomDataStore(UserListDataStore())
CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
)
}

View file

@ -19,11 +19,15 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider
@ -44,6 +48,8 @@ import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
@ -52,6 +58,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.io.File
import java.util.Optional
private const val AN_URI_FROM_CAMERA = "content://uri_from_camera"
private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2"
@ -95,21 +102,21 @@ class ConfigureRoomPresenterTest {
presenter.test {
val initialState = initialState()
var config = initialState.config
assertThat(initialState.config.isValid).isFalse()
assertThat(initialState.isValid).isFalse()
// Room name not empty
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
var newState: ConfigureRoomState = awaitItem()
config = config.copy(roomName = A_ROOM_NAME)
assertThat(newState.config).isEqualTo(config)
assertThat(newState.config.isValid).isTrue()
assertThat(newState.isValid).isTrue()
// Clear room name
newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
newState = awaitItem()
config = config.copy(roomName = null)
assertThat(newState.config).isEqualTo(config)
assertThat(newState.config.isValid).isFalse()
assertThat(newState.isValid).isFalse()
}
}
@ -118,8 +125,9 @@ class ConfigureRoomPresenterTest {
val userListDataStore = UserListDataStore()
val pickerProvider = FakePickerProvider()
val permissionsPresenter = FakePermissionsPresenter()
val roomAliasHelper = FakeRoomAliasHelper()
val presenter = createConfigureRoomPresenter(
createRoomDataStore = CreateRoomDataStore(userListDataStore),
createRoomDataStore = CreateRoomDataStore(userListDataStore, roomAliasHelper),
pickerProvider = pickerProvider,
permissionsPresenter = permissionsPresenter,
)
@ -191,8 +199,7 @@ class ConfigureRoomPresenterTest {
newState = awaitItem()
expectedConfig = expectedConfig.copy(
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled(expectedConfig.roomName ?: ""),
roomAddressErrorState = RoomAddressErrorState.None,
roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")),
roomAccess = RoomAccess.Anyone,
)
)
@ -254,7 +261,7 @@ class ConfigureRoomPresenterTest {
val matrixClient = createMatrixClient()
val analyticsService = FakeAnalyticsService()
val mediaPreProcessor = FakeMediaPreProcessor()
val createRoomDataStore = CreateRoomDataStore(UserListDataStore())
val createRoomDataStore = CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
val presenter = createConfigureRoomPresenter(
createRoomDataStore = createRoomDataStore,
mediaPreProcessor = mediaPreProcessor,
@ -315,17 +322,88 @@ class ConfigureRoomPresenterTest {
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - address is invalid when format is invalid`() = runTest {
val aliasHelper = FakeRoomAliasHelper(
isRoomAliasValidLambda = { false }
)
val presenter = createConfigureRoomPresenter(
roomAliasHelper = aliasHelper
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.InvalidSymbols)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - address is not available when alias is not available`() = runTest {
val fakeMatrixClient = createMatrixClient(isAliasAvailable = false)
val presenter = createConfigureRoomPresenter(
matrixClient = fakeMatrixClient,
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.NotAvailable)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - address is valid when alias is available and format is valid`() = runTest {
val fakeMatrixClient = createMatrixClient(isAliasAvailable = true)
val presenter = createConfigureRoomPresenter(
matrixClient = fakeMatrixClient,
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.Valid)
}
}
}
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
skipItems(1)
return awaitItem()
}
private fun createMatrixClient() = FakeMatrixClient(
private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient(
userIdServerNameLambda = { "matrix.org" },
resolveRoomAliasResult = {
val resolvedRoomAlias = if (isAliasAvailable) {
Optional.empty()
} else {
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
}
Result.success(resolvedRoomAlias)
}
)
private fun createConfigureRoomPresenter(
createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore()),
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore(), roomAliasHelper),
matrixClient: MatrixClient = createMatrixClient(),
pickerProvider: PickerProvider = FakePickerProvider(),
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
@ -339,6 +417,7 @@ class ConfigureRoomPresenterTest {
mediaPreProcessor = mediaPreProcessor,
analyticsService = analyticsService,
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
roomAliasHelper = roomAliasHelper,
featureFlagService = FakeFeatureFlagService(
mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled)
)

View file

@ -9,7 +9,9 @@
package io.element.android.features.logout.impl
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
@ -52,19 +54,18 @@ import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrgani
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.components.list.SwitchListItem
import io.element.android.libraries.designsystem.modifiers.autofill
import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.components.autofill
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
@ -257,26 +258,21 @@ private fun Content(
)
}
Column(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
) {
Text(
text = stringResource(CommonStrings.action_confirm_password),
style = ElementTheme.typography.fontBodySmMedium,
color = ElementTheme.colors.textSecondary,
)
var passwordVisible by remember { mutableStateOf(false) }
if (isLoading) {
// Ensure password is hidden when user submits the form
passwordVisible = false
}
OutlinedTextField(
TextField(
value = passwordFieldState,
label = stringResource(CommonStrings.action_confirm_password),
readOnly = isLoading,
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
.onTabOrEnterKeyFocusNext(focusManager)
.testTag(TestTags.loginPassword)
@ -293,9 +289,7 @@ private fun Content(
passwordFieldState = sanitized
eventSink(AccountDeactivationEvents.SetPassword(sanitized))
},
placeholder = {
Text(text = stringResource(CommonStrings.common_password))
},
placeholder = stringResource(CommonStrings.common_password),
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
val image =
@ -303,7 +297,7 @@ private fun Content(
val description =
if (passwordVisible) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password)
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Box(modifier = Modifier.clickable { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, description)
}
},

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Vahvista, että haluat deaktivoida tilisi. Tätä ei voi perua."</string>
<string name="screen_deactivate_account_delete_all_messages">"Poista kaikki viestini"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Varoitus: Tulevaisuudessa muut voivat nähdä puutteellisia keskusteluja."</string>
<string name="screen_deactivate_account_description">"Tilisi deaktivointia %1$s. Jos teet sen:"</string>
<string name="screen_deactivate_account_description_bold_part">"ei voi peruuttaa"</string>
<string name="screen_deactivate_account_list_item_1">"Tilisi %1$s (et voi kirjautua takaisin sisään, eikä tunnustasi voi käyttää uudelleen)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"poistetaan käytöstä pysyvästi"</string>
<string name="screen_deactivate_account_list_item_2">"Sinut poistetaan kaikista keskusteluhuoneista."</string>
<string name="screen_deactivate_account_list_item_3">"Tilitietosi poistetaan identiteettipalvelimeltamme."</string>
<string name="screen_deactivate_account_list_item_4">"Viestisi näkyvät edelleen rekisteröityneille käyttäjille, mutta ne eivät ole uusien tai rekisteröimättömien käyttäjien saatavilla, jos päätät poistaa ne."</string>
<string name="screen_deactivate_account_title">"Deaktivoi tili"</string>
</resources>

View file

@ -9,6 +9,6 @@
<string name="screen_deactivate_account_list_item_1_bold_part">"Desativar permanentemente"</string>
<string name="screen_deactivate_account_list_item_2">"Removê-lo de todas as salas de chat."</string>
<string name="screen_deactivate_account_list_item_3">"Exclua as informações da sua conta do nosso servidor de identidade."</string>
<string name="screen_deactivate_account_list_item_4">"Suas mensagens ainda estarão visíveis para usuários registrados, mas não estarão disponíveis para usuários novos ou não registrados se você optar por excluí-las."</string>
<string name="screen_deactivate_account_list_item_4">"As tuas mensagens continuarão a ser visíveis para os utilizadores registados, mas não estarão disponíveis para os utilizadores novos ou não registados se optares por as apagar."</string>
<string name="screen_deactivate_account_title">"Desativar conta"</string>
</resources>

View file

@ -6,7 +6,7 @@
<string name="screen_deactivate_account_description">"Отключение вашей учетной записи %1$s и означает следующее:"</string>
<string name="screen_deactivate_account_description_bold_part">"необратимо"</string>
<string name="screen_deactivate_account_list_item_1">"Ваша учётная запись будет %1$s (вы не сможете войти в неё снова, и ваш ID не может быть использован повторно)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Отключить навсегда"</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"отключена навсегда"</string>
<string name="screen_deactivate_account_list_item_2">"Вы будете удалены из всех чатов."</string>
<string name="screen_deactivate_account_list_item_3">"Данные вашей учётной записи будут удалены с нашего сервера идентификации."</string>
<string name="screen_deactivate_account_list_item_4">"Ваши сообщения по-прежнему будут видны зарегистрированным пользователям, но не будут доступны новым или незарегистрированным пользователям, если вы решите удалить их."</string>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Будь ласка, підтвердіть, що ви хочете деактивувати свій обліковий запис. Ця дія не може бути скасована."</string>
<string name="screen_deactivate_account_delete_all_messages">"Видалити всі мої повідомлення"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Попередження: майбутні користувачі можуть бачити неповні розмови."</string>
<string name="screen_deactivate_account_description">"Деактивація вашого облікового запису%1$s , це буде:"</string>
<string name="screen_deactivate_account_description_bold_part">"незворотні"</string>
<string name="screen_deactivate_account_list_item_1">"%1$sваш обліковий запис (ви не можете знову увійти, а ваш ідентифікатор не може бути використаний повторно)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Назавжди відключити"</string>
<string name="screen_deactivate_account_list_item_2">"Видалити вас з усіх чатів."</string>
<string name="screen_deactivate_account_list_item_3">"Видаліть інформацію свого облікового запису з нашого сервера ідентифікації."</string>
<string name="screen_deactivate_account_list_item_4">"Ваші повідомлення залишатимуться видимими для зареєстрованих користувачів, але недоступними для нових або незареєстрованих користувачів, якщо ви вирішите їх видалити."</string>
<string name="screen_deactivate_account_title">"Відключити обліковий запис"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"请确认您要停用您的账户。此操作无法撤消。"</string>
<string name="screen_deactivate_account_delete_all_messages">"删除我的所有消息"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"警告:未来的用户可能会看到不完整的对话。"</string>
<string name="screen_deactivate_account_description">"停用您的帐户是%1$s它将"</string>
<string name="screen_deactivate_account_description_bold_part">"不可逆转的"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s您的账户您无法登录回来并且您的ID无法重复使用。"</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"永久禁用"</string>
<string name="screen_deactivate_account_list_item_2">"将您从所有聊天房间中移除。"</string>
<string name="screen_deactivate_account_list_item_3">"从我们的身份服务器中删除您的账户信息。"</string>
<string name="screen_deactivate_account_list_item_4">"注册用户仍可看到您的消息,但如果您选择删除它们,新用户或未注册用户将无法看到您的消息。"</string>
<string name="screen_deactivate_account_title">"停用账户"</string>
</resources>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Voit muuttaa asetuksia myöhemmin."</string>
<string name="screen_notification_optin_title">"Salli ilmoitukset ja älä koskaan missaa viestejä"</string>
<string name="screen_welcome_bullet_1">"Puhelut, kyselyt, haku ja paljon muuta lisätään myöhemmin tänä vuonna."</string>
<string name="screen_welcome_bullet_2">"Salattujen huoneiden viestihistoria ei ole vielä käytettävissä."</string>
<string name="screen_welcome_bullet_3">"Haluaisimme kuulla mielipiteesi, kerro mitä mieltä olet asetuksien kautta."</string>
<string name="screen_welcome_button">"Mennään!"</string>
<string name="screen_welcome_subtitle">"Tässä on mitä sinun tarvitsee tietää:"</string>
<string name="screen_welcome_title">"Tervetuloa %1$s -sovellukseen!"</string>
</resources>

View file

@ -2,10 +2,10 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Ви можете змінити свої налаштування пізніше."</string>
<string name="screen_notification_optin_title">"Дозволити сповіщення і ніколи не пропускати повідомлення"</string>
<string name="screen_welcome_bullet_1">"Дзвінки, опитування, пошук тощо будуть додані пізніше цього року."</string>
<string name="screen_welcome_bullet_1">"Виклики, опитування, пошук тощо будуть додані пізніше цього року."</string>
<string name="screen_welcome_bullet_2">"Історія повідомлень для зашифрованих кімнат ще недоступна."</string>
<string name="screen_welcome_bullet_3">"Ми хотіли б почути вас, розкажіть нам ваші враження та ідеї щодо застосунку на сторінці налаштувань."</string>
<string name="screen_welcome_button">"Пішли!"</string>
<string name="screen_welcome_button">"Уперед!"</string>
<string name="screen_welcome_subtitle">"Ось що вам потрібно знати:"</string>
<string name="screen_welcome_title">"Ласкаво просимо до %1$s!"</string>
</resources>

View file

@ -3,7 +3,7 @@
<string name="screen_notification_optin_subtitle">"您可以稍后更改设置。"</string>
<string name="screen_notification_optin_title">"允许通知,绝不错过任何消息"</string>
<string name="screen_welcome_bullet_1">"今年晚些时候将增加通话、投票、搜索等功能。"</string>
<string name="screen_welcome_bullet_2">"加密房间的消息历史记录尚不可用。"</string>
<string name="screen_welcome_bullet_2">"加密聊天室的消息历史记录尚不可用。"</string>
<string name="screen_welcome_bullet_3">"我们很乐意听取您的意见,请通过设置页面告诉我们您的想法。"</string>
<string name="screen_welcome_button">"开始吧!"</string>
<string name="screen_welcome_subtitle">"以下是您需要了解的内容:"</string>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Haluatko varmasti hylätä kutsun liittyä %1$s -huoneeseen?"</string>
<string name="screen_invites_decline_chat_title">"Hylkää kutsu"</string>
<string name="screen_invites_decline_direct_chat_message">"Haluatko varmasti hylätä kutsun yksityiseen keskusteluun käyttäjän %1$s kanssa?"</string>
<string name="screen_invites_decline_direct_chat_title">"Hylkää keskustelu"</string>
<string name="screen_invites_empty_list">"Ei kutsuja"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) kutsui sinut"</string>
</resources>

View file

@ -2,8 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Відхилити запрошення"</string>
<string name="screen_invites_decline_direct_chat_message">"Ви дійсно хочете відмовитися від приватного чату з %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Відхилити чат"</string>
<string name="screen_invites_decline_direct_chat_message">"Ви дійсно хочете відмовитися від приватної бесіди з %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Відхилити бесіду"</string>
<string name="screen_invites_empty_list">"Немає запрошень"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) запросив (-ла) Вас"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) запрошує вас"</string>
</resources>

View file

@ -42,7 +42,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import io.element.android.libraries.matrix.ui.model.toInviteSender
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -96,10 +96,10 @@ class JoinRoomPresenter @AssistedInject constructor(
}
else -> {
value = ContentState.Loading(roomIdOrAlias)
val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
val result = matrixClient.getRoomPreviewInfo(roomIdOrAlias, serverNames)
value = result.fold(
onSuccess = { roomPreview ->
roomPreview.toContentState()
onSuccess = { previewInfo ->
previewInfo.toContentState()
},
onFailure = { throwable ->
if (throwable.message?.contains("403") == true) {
@ -184,7 +184,7 @@ class JoinRoomPresenter @AssistedInject constructor(
}
}
private fun RoomPreview.toContentState(): ContentState {
private fun RoomPreviewInfo.toContentState(): ContentState {
return ContentState.Loaded(
roomId = roomId,
name = name,

View file

@ -60,8 +60,8 @@ 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.ButtonSize
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.RoomType
@ -390,19 +390,13 @@ private fun DefaultLoadedContent(
)
} else if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
Spacer(modifier = Modifier.height(24.dp))
OutlinedTextField(
TextField(
value = knockMessage,
onValueChange = onKnockMessageUpdate,
maxLines = 3,
minLines = 3,
modifier = Modifier.fillMaxWidth()
)
Text(
text = stringResource(R.string.screen_join_room_knock_message_description),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textPlaceholder,
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
supportingText = stringResource(R.string.screen_join_room_knock_message_description)
)
}
}

View file

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_cancel_knock_action">"Ακύρωση αιτήματος"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ναι, ακύρωση"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Σίγουρα θες να ακυρώσεις το αίτημά σου για συμμετοχή σε αυτό το δωμάτιο;"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Ακύρωση αίτησης συμμετοχής"</string>
<string name="screen_join_room_join_action">"Συμμετοχή στο δωμάτιο"</string>
<string name="screen_join_room_knock_action">"Χτύπα για συμμετοχή"</string>
<string name="screen_join_room_knock_message_description">"Μήνυμα (προαιρετικό)"</string>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_cancel_knock_action">"Peruuta pyyntö"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Kyllä, peruuta"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Haluatko varmasti peruuttaa pyyntösi liittyä tähän huoneeseen?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Peruuta liittymispyyntö"</string>
<string name="screen_join_room_join_action">"Liity huoneeseen"</string>
<string name="screen_join_room_knock_action">"Lähetä liittymispyyntö"</string>
<string name="screen_join_room_knock_message_description">"Viesti (valinnainen)"</string>
<string name="screen_join_room_knock_sent_description">"Saat kutsun liittyä huoneeseen, jos pyyntösi hyväksytään."</string>
<string name="screen_join_room_knock_sent_title">"Liittymispyyntö lähetetty"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s ei tue vielä tiloja. Voit käyttää tiloja selainversiolla."</string>
<string name="screen_join_room_space_not_supported_title">"Tiloja ei vielä tueta"</string>
<string name="screen_join_room_subtitle_knock">"Paina alla olevaa nappia ja huoneen ylläpitäjä saa ilmoituksen. Voit liittyä keskusteluun kun pyyntösi on hyväksytty."</string>
<string name="screen_join_room_subtitle_no_preview">"Sinun on oltava tämän huoneen jäsen, jotta voit nähdä viestihistorian."</string>
<string name="screen_join_room_title_knock">"Haluatko liittyä tähän huoneeseen?"</string>
<string name="screen_join_room_title_no_preview">"Esikatselu ei ole saatavilla"</string>
</resources>

View file

@ -1,8 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_cancel_knock_action">"Скасувати запит"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Так, скасувати"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Ви впевнені, що бажаєте скасувати свій запит на приєднання до цієї кімнати?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Скасувати запит на приєднання"</string>
<string name="screen_join_room_join_action">"Приєднатися до кімнати"</string>
<string name="screen_join_room_knock_action">"Постукати, щоб приєднатися"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s ще не підтримує простори. Ви можете отримати доступ до них в вебверсії."</string>
<string name="screen_join_room_knock_message_description">"Повідомлення (необов\'язково)"</string>
<string name="screen_join_room_knock_sent_description">"Ви отримаєте запрошення приєднатися до кімнати, якщо ваш запит буде прийнятий."</string>
<string name="screen_join_room_knock_sent_title">"Запит на приєднання надіслано"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s ще не підтримує простори. Ви можете отримати доступ до них у вебверсії."</string>
<string name="screen_join_room_space_not_supported_title">"Простори поки що не підтримуються"</string>
<string name="screen_join_room_subtitle_knock">"Натисніть кнопку нижче, і адміністратор кімнати отримає сповіщення. Ви зможете приєднатися до розмови після схвалення."</string>
<string name="screen_join_room_subtitle_no_preview">"Ви мусите бути учасником цієї кімнати, щоб переглядати історію повідомлень."</string>

View file

@ -1,11 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_cancel_knock_action">"取消请求"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"是的,取消"</string>
<string name="screen_join_room_cancel_knock_alert_description">"您确定要取消加入此房间的请求吗?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"取消加入申请"</string>
<string name="screen_join_room_join_action">"加入聊天室"</string>
<string name="screen_join_room_knock_action">"加入房间"</string>
<string name="screen_join_room_knock_action">"加入聊天室"</string>
<string name="screen_join_room_knock_message_description">"消息(可选)"</string>
<string name="screen_join_room_knock_sent_description">"如果您的请求被接受,您将收到加入房间的邀请。"</string>
<string name="screen_join_room_knock_sent_title">"加入请求已发送"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s 尚不支持空间。您可以通过 Web 端访问空间"</string>
<string name="screen_join_room_space_not_supported_title">"空间尚不支持"</string>
<string name="screen_join_room_subtitle_knock">"点击下面的按钮,系统将通知房间管理员。获得批准后,您将能够加入对话。"</string>
<string name="screen_join_room_subtitle_knock">"点击下面的按钮,系统将通知聊天室管理员。获得批准后将能够加入对话。"</string>
<string name="screen_join_room_subtitle_no_preview">"只有聊天室成员才能查看消息历史记录。"</string>
<string name="screen_join_room_title_knock">"想加入这个房间吗?"</string>
<string name="screen_join_room_title_knock">"想加入这个聊天室吗?"</string>
<string name="screen_join_room_title_no_preview">"预览不可用"</string>
</resources>

View file

@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
@ -408,9 +408,9 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is not known RoomPreview is loaded`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
getRoomPreviewInfoResult = { _, _ ->
Result.success(
RoomPreview(
RoomPreviewInfo(
roomId = A_ROOM_ID,
canonicalAlias = RoomAlias("#alias:matrix.org"),
name = "Room name",
@ -453,7 +453,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
getRoomPreviewInfoResult = { _, _ ->
Result.failure(AN_EXCEPTION)
}
)
@ -491,7 +491,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
getRoomPreviewInfoResult = { _, _ ->
Result.failure(Exception("403"))
}
)

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Haluatko varmasti poistua keskustelusta? Tämä keskustelu ei ole julkinen ja et voi liittyä takaisin ilman kutsua."</string>
<string name="leave_room_alert_empty_subtitle">"Haluatko varmasti poistua huoneesta? Olet huoneen ainoa jäsen. Jos poistut, kukaan ei voi liittyä takaisin, et edes sinä."</string>
<string name="leave_room_alert_private_subtitle">"Haluatko varmasti poistua huoneesta? Tämä huone ei ole julkinen ja et voi liittyä takaisin ilman kutsua."</string>
<string name="leave_room_alert_subtitle">"Haluatko varmasti poistua huoneesta?"</string>
</resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Ви впевнені, що хочете залишити цю розмову? Ця розмова не є загальнодоступною, і ви не зможете знову приєднатися без запрошення."</string>
<string name="leave_room_alert_empty_subtitle">"Ви впевнені, що хочете вийти з цієї кімнати? Ви тут єдина людина. Якщо Ви вийдете, ніхто в майбутньому не зможе приєднатися, у тому числі і Ви."</string>
<string name="leave_room_alert_private_subtitle">"Ви впевнені, що хочете вийти з цієї кімнати? Ця кімната не є публічною, і ви не зможете повернутися до неї без запрошення."</string>
<string name="leave_conversation_alert_subtitle">"Ви впевнені, що хочете залишити цю розмову? Ця розмова не загальнодоступна, і ви не зможете знову приєднатися без запрошення."</string>
<string name="leave_room_alert_empty_subtitle">"Ви впевнені, що хочете вийти з цієї кімнати? Ви тут єдина людина. Якщо ви вийдете, ніхто в майбутньому не зможе приєднатися, у тому числі й ви."</string>
<string name="leave_room_alert_private_subtitle">"Ви впевнені, що хочете вийти з цієї кімнати? Ця кімната не загальнодоступна, і ви не зможете повернутися до неї без запрошення."</string>
<string name="leave_room_alert_subtitle">"Ви впевнені, що хочете вийти з кімнати?"</string>
</resources>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"您確定要離開對話嗎?此對話不是公開的,如果沒有收到邀請,您無法重新加入。"</string>
<string name="leave_room_alert_empty_subtitle">"您確定要離開聊天室嗎?這裡只有您一個人。如果您離開了,包含您在內的所有人都無法再進入此聊天室。"</string>
<string name="leave_room_alert_private_subtitle">"您確定要離開聊天室嗎?此聊天室不是公開的,如果沒有收到邀請,您無法重新加入。"</string>
<string name="leave_room_alert_subtitle">"您確定要離開聊天室嗎?"</string>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"您确定要离开此对话吗?此对话不公开,未经邀请您将无法重新加入。"</string>
<string name="leave_room_alert_empty_subtitle">"确定要离开这个房间吗?这里只有你一个人。如果你离开此房间,包括你在内的所有人都将无法进入。"</string>
<string name="leave_room_alert_private_subtitle">"确定要离开这个房间吗?此房间不公开,没有邀请你将无法重新加入。"</string>
<string name="leave_room_alert_subtitle">"确定要离开房间吗?"</string>
<string name="leave_room_alert_empty_subtitle">"确定要离开此聊天室吗?此处只有你一个人。如果离开此聊天室,包括你在内的所有人都将无法进入。"</string>
<string name="leave_room_alert_private_subtitle">"确定要离开此聊天室吗?此聊天室不公开,没有邀请你将无法重新加入。"</string>
<string name="leave_room_alert_subtitle">"确定要离开聊天室吗?"</string>
</resources>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biometrinen tunnistus"</string>
<string name="screen_app_lock_biometric_unlock">"biometrinen tunnistus"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Avaa biometrisellä"</string>
<string name="screen_app_lock_forgot_pin">"Unohtuiko PIN-koodi?"</string>
<string name="screen_app_lock_settings_change_pin">"Vaihda PIN-koodi"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Salli biometrinen tunnistus"</string>
<string name="screen_app_lock_settings_remove_pin">"Poista PIN-koodi"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Haluatko varmasti poistaa PIN-koodin?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Poista PIN-koodi?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Salli %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Käytän mieluummin PIN-koodia"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Säästä aikaa ja ota käyttöön %1$s"</string>
<string name="screen_app_lock_setup_choose_pin">"Valitse PIN-koodi"</string>
<string name="screen_app_lock_setup_confirm_pin">"Vahvista PIN-koodi"</string>
<string name="screen_app_lock_setup_pin_context">"Lukitse %1$s -sovellus lisätäksesi turvaa keskusteluihisi.
Valitse PIN-koodi, jonka muistat. Jos unohdat sen, joudut kirjautumaan ulos."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Et voi valita tätä PIN-koodia turvallisuussyistä"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Valitse toinen PIN-koodi"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Anna sama PIN-koodi kahdesti"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-koodit eivät täsmää"</string>
<string name="screen_app_lock_signout_alert_message">"Sinun on kirjauduttava sisään uudelleen ja luotava uusi PIN-koodi jatkaaksesi"</string>
<string name="screen_app_lock_signout_alert_title">"Sinut kirjataan ulos"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Sinulla on %1$d yritys"</item>
<item quantity="other">"Sinulla on %1$d yritystä"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Väärä PIN-koodi. Sinulla on %1$d yritys jäljellä"</item>
<item quantity="other">"Väärä PIN-koodi. Sinulla on %1$d yritystä jäljellä"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Käytä biometristä"</string>
<string name="screen_app_lock_use_pin_android">"Käytä PIN-koodia"</string>
<string name="screen_signout_in_progress_dialog_content">"Kirjaudutaan ulos…"</string>
</resources>

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"біометрична аутентифікація"</string>
<string name="screen_app_lock_biometric_authentication">"біометрична автентифікація"</string>
<string name="screen_app_lock_biometric_unlock">"біометричне розблокування"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Розблокуйте за допомогою біометрії"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Розблокувати за допомогою біометрії"</string>
<string name="screen_app_lock_forgot_pin">"Забули PIN-код?"</string>
<string name="screen_app_lock_settings_change_pin">"Змінити PIN-код"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Дозволити біометричне розблокування"</string>
@ -10,18 +10,18 @@
<string name="screen_app_lock_settings_remove_pin_alert_message">"Ви впевнені, що хочете видалити PIN-код?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Видалити PIN-код?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Дозволити %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Я б краще використав PIN-код"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Мені краще використати PIN-код"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Заощаджуйте час і використовуйте %1$s для розблокування застосунку щоразу"</string>
<string name="screen_app_lock_setup_choose_pin">"Виберіть PIN-код"</string>
<string name="screen_app_lock_setup_confirm_pin">"Підтвердити PIN-код"</string>
<string name="screen_app_lock_setup_pin_context">"Заблокуйте %1$s, щоб додати додаткову безпеку вашим чатам.
Виберіть щось, що запам\'ятовується. Але якщо ви забудете PIN-код, ви вийдете з застосунку."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Ви не можете вибрати його як свій PIN-код з міркувань безпеки"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Ви не можете вибрати його своїм PIN-кодом з міркувань безпеки"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Виберіть інший PIN-код"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Будь ласка, введіть один і той самий PIN-код двічі"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-коди не збігаються"</string>
<string name="screen_app_lock_signout_alert_message">"Щоб продовжити, вам потрібно буде повторно увійти та створити новий PIN-код"</string>
<string name="screen_app_lock_signout_alert_message">"Щоб продовжити, вам потрібно повторно ввійти та створити новий PIN-код"</string>
<string name="screen_app_lock_signout_alert_title">"Ви виходите з системи"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Ви маєте %1$d спробу"</item>
@ -33,7 +33,7 @@
<item quantity="few">"Хибний PIN-код. Ви маєте ще %1$d шанси"</item>
<item quantity="many">"Хибний PIN-код. Ви маєте ще %1$d шансів"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Використовуйте біометрію"</string>
<string name="screen_app_lock_use_pin_android">"Використовуйте PIN-код"</string>
<string name="screen_app_lock_use_biometric_android">"Використати біометрію"</string>
<string name="screen_app_lock_use_pin_android">"Використати PIN-код"</string>
<string name="screen_signout_in_progress_dialog_content">"Вихід…"</string>
</resources>

View file

@ -7,6 +7,7 @@
package io.element.android.features.login.impl.screens.loginpassword
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@ -52,17 +53,15 @@ import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.modifiers.autofill
import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.components.autofill
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
@ -101,12 +100,12 @@ fun LoginPasswordView(
Column(
modifier = Modifier
.fillMaxSize()
.imePadding()
.padding(padding)
.consumeWindowInsets(padding)
.verticalScroll(state = scrollState)
.padding(start = 20.dp, end = 20.dp, bottom = 20.dp),
.fillMaxSize()
.imePadding()
.padding(padding)
.consumeWindowInsets(padding)
.verticalScroll(state = scrollState)
.padding(start = 20.dp, end = 20.dp, bottom = 20.dp),
) {
// Title
IconTitleSubtitleMolecule(
@ -140,8 +139,8 @@ fun LoginPasswordView(
onClick = ::submit,
enabled = state.submitEnabled || isLoading,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginContinue)
.fillMaxWidth()
.testTag(TestTags.loginContinue)
)
Spacer(modifier = Modifier.height(48.dp))
}
@ -170,16 +169,10 @@ private fun LoginForm(
val eventSink = state.eventSink
Column {
Text(
text = stringResource(R.string.screen_login_form_header),
modifier = Modifier.padding(start = 16.dp),
style = ElementTheme.typography.fontBodyMdRegular,
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
TextField(
label = stringResource(R.string.screen_login_form_header),
value = loginFieldState,
readOnly = isLoading,
enabled = !isLoading,
modifier = Modifier
.fillMaxWidth()
.onTabOrEnterKeyFocusNext(focusManager)
@ -192,9 +185,7 @@ private fun LoginForm(
eventSink(LoginPasswordEvents.SetLogin(sanitized))
}
),
placeholder = {
Text(text = stringResource(CommonStrings.common_username))
},
placeholder = stringResource(CommonStrings.common_username),
onValueChange = {
val sanitized = it.sanitize()
loginFieldState = sanitized
@ -210,10 +201,14 @@ private fun LoginForm(
singleLine = true,
trailingIcon = if (loginFieldState.isNotEmpty()) {
{
IconButton(onClick = {
Box(Modifier.clickable {
loginFieldState = ""
}) {
Icon(imageVector = CompoundIcons.Close(), contentDescription = stringResource(CommonStrings.action_clear))
Icon(
imageVector = CompoundIcons.Close(),
contentDescription = stringResource(CommonStrings.action_clear),
tint = ElementTheme.colors.iconSecondary
)
}
}
} else {
@ -226,9 +221,9 @@ private fun LoginForm(
passwordVisible = false
}
Spacer(Modifier.height(20.dp))
OutlinedTextField(
TextField(
value = passwordFieldState,
readOnly = isLoading,
enabled = !isLoading,
modifier = Modifier
.fillMaxWidth()
.onTabOrEnterKeyFocusNext(focusManager)
@ -246,18 +241,18 @@ private fun LoginForm(
passwordFieldState = sanitized
eventSink(LoginPasswordEvents.SetPassword(sanitized))
},
placeholder = {
Text(text = stringResource(CommonStrings.common_password))
},
placeholder = stringResource(CommonStrings.common_password),
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
val image =
if (passwordVisible) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff()
val description =
if (passwordVisible) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password)
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, description)
Box(Modifier.clickable { passwordVisible = !passwordVisible }) {
Icon(
imageVector = image,
contentDescription = description,
)
}
},
keyboardOptions = KeyboardOptions(

View file

@ -9,6 +9,7 @@
package io.element.android.features.login.impl.screens.searchaccountprovider
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
@ -23,7 +24,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
@ -51,14 +51,12 @@ import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubti
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
@ -86,10 +84,10 @@ fun SearchAccountProviderView(
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.imePadding()
.padding(padding)
.consumeWindowInsets(padding)
.fillMaxSize()
.imePadding()
.padding(padding)
.consumeWindowInsets(padding)
) {
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
item {
@ -104,7 +102,7 @@ fun SearchAccountProviderView(
// TextInput
var userInputState by textFieldState(stateValue = state.userInput)
val focusManager = LocalFocusManager.current
OutlinedTextField(
TextField(
value = userInputState,
// readOnly = isLoading,
modifier = Modifier
@ -126,7 +124,7 @@ fun SearchAccountProviderView(
singleLine = true,
trailingIcon = if (userInputState.isNotEmpty()) {
{
IconButton(onClick = {
Box(Modifier.clickable {
userInputState = ""
eventSink(SearchAccountProviderEvents.UserInput(""))
}) {
@ -139,9 +137,7 @@ fun SearchAccountProviderView(
} else {
null
},
supportingText = {
Text(text = stringResource(id = R.string.screen_account_provider_form_notice), color = MaterialTheme.colorScheme.secondary)
}
supportingText = stringResource(id = R.string.screen_account_provider_form_notice),
)
}

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Vaihda palveluntarjoajaa"</string>
<string name="screen_account_provider_form_hint">"Kotipalvelimen osoite"</string>
<string name="screen_account_provider_form_notice">"Kirjoita hakutermi tai osoite."</string>
<string name="screen_account_provider_form_subtitle">"Hae yritystä, yhteisöä tai yksityistä palvelinta."</string>
<string name="screen_account_provider_form_title">"Etsi palveluntarjoajaa"</string>
<string name="screen_account_provider_signin_subtitle">"Keskustelusi asuvat täällä — aivan kuten aivan kuten käyttäisit sähköpostipalveluntarjoajaa sähköpostiesi säilyttämiseen."</string>
<string name="screen_account_provider_signin_title">"Olet kirjautumassa sisään %s-palvelimelle"</string>
<string name="screen_account_provider_signup_subtitle">"Keskustelusi asuvat täällä — aivan kuten aivan kuten käyttäisit sähköpostipalveluntarjoajaa sähköpostiesi säilyttämiseen."</string>
<string name="screen_account_provider_signup_title">"Olet luomassa tiliä %s-palvelimelle"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org on suuri, ilmainen palvelin julkisessa Matrix-verkossa turvalliseen, hajautettuun viestintään, jota ylläpitää Matrix.org-säätiö."</string>
<string name="screen_change_account_provider_other">"Muu"</string>
<string name="screen_change_account_provider_subtitle">"Käytä toista palveluntarjoajaa, kuten omaa yksityistä palvelintasi tai työpaikkaasi."</string>
<string name="screen_change_account_provider_title">"Vaihda palveluntarjoajaa"</string>
<string name="screen_change_server_error_invalid_homeserver">"Kotipalvelimeen ei saatu yhteyttä. Varmista, että olet syöttänyt osoitteen oikein. Jos osoite on oikein, ota yhteyttä palvelimesi ylläpitäjään."</string>
<string name="screen_change_server_error_invalid_well_known">"Sliding sync ei ole saatavilla well-known tiedostossa olevan ongelman vuoksi:
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Tämä palvelin ei tällä hetkellä tue sliding syncia."</string>
<string name="screen_change_server_form_header">"Kotipalvelimen osoite"</string>
<string name="screen_change_server_form_notice">"Voit yhdistää vain olemassa olevaan palvelimeen, joka tukee sliding syncia. Kotipalvelimesi ylläpitäjän on otettava se käyttöön. %1$s"</string>
<string name="screen_change_server_subtitle">"Mikä on palvelimesi osoite?"</string>
<string name="screen_change_server_title">"Valitse palvelimesi"</string>
<string name="screen_create_account_title">"Luo tili"</string>
<string name="screen_login_error_deactivated_account">"Tämä tili on deaktivoitu."</string>
<string name="screen_login_error_invalid_credentials">"Väärä käyttäjänimi ja/tai salasana"</string>
<string name="screen_login_error_invalid_user_id">"Tämä ei ole kelvollinen käyttäjätunnus. Odotettu muoto: \'@käyttäjä:kotipalvelin.fi\'"</string>
<string name="screen_login_error_refresh_tokens">"Tämä palvelin on määritetty käyttämään refresh tokeneja. Näitä ei tueta salasanapohjaisen kirjautumisen kanssa."</string>
<string name="screen_login_error_unsupported_authentication">"Valitsemasi kotipalvelin ei tue salasana- tai OIDC-kirjautumista. Ota yhteyttä palvelimesi ylläpitäjään tai valitse toinen kotipalvelin."</string>
<string name="screen_login_form_header">"Syötä tietosi"</string>
<string name="screen_login_subtitle">"Matrix on avoin verkko turvallista, hajautettua viestintää varten."</string>
<string name="screen_login_title">"Tervetuloa takaisin!"</string>
<string name="screen_login_title_with_homeserver">"Kirjaudu sisään %1$s -palvelimelle"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Muodostetaan turvallista yhteyttä"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Turvallista yhteyttä uuteen laitteeseen ei voitu muodostaa. Olemassa olevat laitteesi ovat edelleen turvassa, eikä sinun tarvitse huolehtia niistä."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Mitä nyt?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Yritä kirjautua sisään uudelleen QR-koodilla, jos kyseessä oli verkko-ongelma"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Jos kohtaat saman ongelman, kokeile toista wifi-verkkoa tai käytä mobiilidataa wifi-yhteyden sijaan"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Jos tämä ei auta, kirjaudu sisään manuaalisesti"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"Yhteys ei ole turvallinen"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Sinua pyydetään antamaan tässä laitteessa näkyvät kaksi numeroa."</string>
<string name="screen_qr_code_login_device_code_title">"Kirjoita alla oleva numero toisella laitteellasi"</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"Kirjaudu sisään toisella laitteellasi ja yritä sitten uudelleen tai käytä toista laitetta, joka on jo kirjautunut sisään."</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Toinen laitteesi ei ole kirjautuneena"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Kirjautuminen peruutettiin toisella laitteella."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Kirjautumispyyntö peruutettu"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Kirjautuminen hylättiin toisella laitteella."</string>
<string name="screen_qr_code_login_error_declined_title">"Kirjautuminen hylätty"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Kirjautuminen vanhentui. Yritä uudelleen."</string>
<string name="screen_qr_code_login_error_expired_title">"Kirjautumista ei suoritettu ajoissa"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Toinen laitteesi ei tue kirjautumista %s -sovellukseen QR-koodilla.
Yritä kirjautua sisään manuaalisesti tai skannaa QR-koodi toisella laitteella."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-koodia ei tueta"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Palveluntarjoajasi ei tue %1$s -sovellusta"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s -sovellusta ei tueta"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Valmis skannaamaan"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Avaa %1$s tietokoneella"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Napsauta avatariasi"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Valitse %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Yhdistä uusi laite”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Skannaa QR-koodi tällä laitteella"</string>
<string name="screen_qr_code_login_initial_state_subtitle">"Saatavilla vain, jos palveluntarjoajasi tukee sitä."</string>
<string name="screen_qr_code_login_initial_state_title">"Avaa %1$s toisella laitteella saadaksesi QR-koodin"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Käytä toisessa laitteessa näkyvää QR-koodia."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Yritä uudelleen"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Väärä QR-koodi"</string>
<string name="screen_qr_code_login_no_camera_permission_button">"Siirry kameran asetuksiin"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"Jatkaaksesi sinun on annettava lupa %1$s -sovellukselle käyttää laitteesi kameraa."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"Salli lupa kameraan QR-koodin skannaamiseksi"</string>
<string name="screen_qr_code_login_scanning_state_title">"Skannaa QR-koodi"</string>
<string name="screen_qr_code_login_start_over_button">"Aloita alusta"</string>
<string name="screen_qr_code_login_unknown_error_description">"Tapahtui odottamaton virhe. Yritä uudelleen."</string>
<string name="screen_qr_code_login_verify_code_loading">"Odotetaan toista laitettasi"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"Palveluntarjoajasi saattaa kysyä seuraavaa koodia kirjautumisen vahvistamiseksi."</string>
<string name="screen_qr_code_login_verify_code_title">"Vahvistuskoodisi"</string>
<string name="screen_server_confirmation_change_server">"Vaihda palveluntarjoajaa"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Yksityinen palvelin Elementin työntekijöille."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix on avoin verkko turvallista, hajautettua viestintää varten."</string>
<string name="screen_server_confirmation_message_register">"Keskustelusi asuvat täällä — aivan kuten aivan kuten käyttäisit sähköpostipalveluntarjoajaa sähköpostiesi säilyttämiseen."</string>
<string name="screen_server_confirmation_title_login">"Olet kirjautumassa sisään %1$s-palvelimelle"</string>
<string name="screen_server_confirmation_title_register">"Olet luomassa tiliä %1$s-palvelimelle"</string>
</resources>

View file

@ -5,42 +5,43 @@
<string name="screen_account_provider_form_notice">"Уведіть пошуковий термін або адресу домену."</string>
<string name="screen_account_provider_form_subtitle">"Пошук компанії, спільноти або приватного сервера."</string>
<string name="screen_account_provider_form_title">"Знайти провайдера облікового запису"</string>
<string name="screen_account_provider_signin_subtitle">"Тут будуть зберігатися Ваші розмови - так само, як Ви використовуєте поштову скриньку для зберігання своїх електронних листів."</string>
<string name="screen_account_provider_signin_subtitle">"Тут розміщуватимуться ваші розмови — так само як у поштовій скриньці для зберігання своїх електронних листів."</string>
<string name="screen_account_provider_signin_title">"Ви збираєтесь увійти в %s"</string>
<string name="screen_account_provider_signup_subtitle">"Тут будуть зберігатися Ваші розмови - так само, як Ви використовуєте поштову скриньку для зберігання своїх електронних листів."</string>
<string name="screen_account_provider_signup_subtitle">"Тут розміщуватимуться ваші розмови — так само як у поштовій скриньці для зберігання своїх електронних листів."</string>
<string name="screen_account_provider_signup_title">"Ви збираєтеся створити обліковий запис на %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org — це великий безплатний сервер у загальнодоступній мережі Matrix для безпечного децентралізованого зв’язку, яким керує Matrix.org Foundation."</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org — це великий безплатний сервер у загальнодоступній мережі Matrix для безпечного децентралізованого спілкування, яким керує Matrix.org Foundation."</string>
<string name="screen_change_account_provider_other">"Інше"</string>
<string name="screen_change_account_provider_subtitle">"Використати іншого провайдера облікових записів, наприклад, власний приватний сервер або робочий обліковий запис."</string>
<string name="screen_change_account_provider_title">"Змінити провайдера облікового запису"</string>
<string name="screen_change_server_error_invalid_homeserver">"Не вдалося підключитися до цього домашнього сервера. Будь ласка, перевірте, чи правильно Ви ввели URL-адресу домашнього сервера. Якщо URL-адреса правильна, зверніться за додатковою допомогою до адміністратора домашнього сервера."</string>
<string name="screen_change_server_error_invalid_well_known">"Sliding sync недоступний через проблему у well-known файлі:
<string name="screen_change_server_error_invalid_homeserver">"Не вдалося під\'єднатися до цього домашнього сервера. Перевірте правильність введеної URL-адреси домашнього сервера. Якщо URL-адреса правильна, зверніться по додаткову допомогу до адміністратора домашнього сервера."</string>
<string name="screen_change_server_error_invalid_well_known">"Sliding sync недоступний через проблему у файлі well-known:
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Наразі цей сервер не підтримує sliding sync."</string>
<string name="screen_change_server_form_header">"URL-адреса домашнього сервера"</string>
<string name="screen_change_server_form_notice">"Ви можете підключитися лише до наявного сервера, який підтримує sliding sync. Ваш адміністратор домашнього сервера повинен буде налаштувати його. %1$s"</string>
<string name="screen_change_server_subtitle">"Яка адреса Вашого сервера?"</string>
<string name="screen_change_server_form_notice">"Ви можете під\'єднатися лише до наявного сервера, який підтримує sliding sync. Адміністратор вашого домашнього сервера повинен буде налаштувати його. %1$s"</string>
<string name="screen_change_server_subtitle">"Яка адреса вашого сервера?"</string>
<string name="screen_change_server_title">"Виберіть свій сервер"</string>
<string name="screen_create_account_title">"Створити обліковий запис"</string>
<string name="screen_login_error_deactivated_account">"Цей обліковий запис було деактивовано."</string>
<string name="screen_login_error_invalid_credentials">"Неправильне ім\'я користувача та/або пароль"</string>
<string name="screen_login_error_invalid_user_id">"Це недійсний ідентифікатор користувача. Очікуваний формат: \'@user:homeserver.org\'"</string>
<string name="screen_login_error_refresh_tokens">"Цей сервер налаштований на використання токенів оновлення. Вони не підтримуються при використанні входу на основі пароля."</string>
<string name="screen_login_error_refresh_tokens">"Цей сервер налаштований на використання оновлюваних токенів. Вони не підтримуються, якщо використовується вхід за допомогою основі пароля."</string>
<string name="screen_login_error_unsupported_authentication">"Обраний домашній сервер не підтримує вхід за допомогою пароля або OIDC. Зверніться до адміністратора або виберіть інший домашній сервер."</string>
<string name="screen_login_form_header">"Введіть свої дані"</string>
<string name="screen_login_subtitle">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string>
<string name="screen_login_title">"З поверненням!"</string>
<string name="screen_login_title_with_homeserver">"Увійти в %1$s"</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_item_1">"Спробуйте увійти ще раз за допомогою QR-коду, якщо це була проблема з мережею"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Якщо ви зіткнулися з тією ж проблемою, спробуйте іншу мережу Wi-Fi або використовуйте мобільний інтернет замість Wi-Fi"</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_title">"Введіть номер нижче на іншому пристрої"</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"Увійдіть на іншому пристрої та спробуйте ще раз або скористайтеся іншим пристроєм, що вже в обліковому записі."</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Інший пристрій не ввійшов"</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"Увійдіть на іншому пристрої та спробуйте ще раз або скористайтеся іншим пристроєм, на якому ви вже ввійшли."</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Вхід на іншому пристрої не виконано"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Вхід було скасовано на іншому пристрої."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Запит на вхід скасовано"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Вхід був відхилений на іншому пристрої."</string>
@ -56,9 +57,10 @@
<string name="screen_qr_code_login_initial_state_button_title">"Готовий до сканування"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Відкрийте %1$s на комп\'ютері"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Натисніть на свою аватарку"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Оберіть %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Підключити новий пристрій”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Відскануйте QR-код цим пристроєм"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Виберіть %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Під\'єднати новий пристрій”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Зіскануйте QR-код цим пристроєм"</string>
<string name="screen_qr_code_login_initial_state_subtitle">"Доступно лише в тому випадку, якщо ваш постачальник облікового запису підтримує цю функцію."</string>
<string name="screen_qr_code_login_initial_state_title">"Відкрийте %1$s на іншому пристрої, щоб отримати QR-код"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Використовуйте QR-код, показаний на іншому пристрої."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Спробуйте ще раз"</string>
@ -66,7 +68,7 @@
<string name="screen_qr_code_login_no_camera_permission_button">"Перейти до налаштувань камери"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"Вам потрібно дати дозвіл %1$s на використання камери вашого пристрою, щоб продовжити."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"Надайте доступ до камери, щоб сканувати QR-код"</string>
<string name="screen_qr_code_login_scanning_state_title">"Відскануйте QR-код"</string>
<string name="screen_qr_code_login_scanning_state_title">"Зіскануйте QR-код"</string>
<string name="screen_qr_code_login_start_over_button">"Почати спочатку"</string>
<string name="screen_qr_code_login_unknown_error_description">"Сталася несподівана помилка. Будь ласка, спробуйте ще раз."</string>
<string name="screen_qr_code_login_verify_code_loading">"Чекаємо на ваш інший пристрій"</string>
@ -75,7 +77,7 @@
<string name="screen_server_confirmation_change_server">"Змінити провайдера облікового запису"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Приватний сервер для співробітників Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string>
<string name="screen_server_confirmation_message_register">"Тут будуть зберігатися Ваші розмови - так само, як Ви використовуєте поштову скриньку для зберігання своїх електронних листів."</string>
<string name="screen_server_confirmation_message_register">"Тут розміщуватимуться ваші розмови — так само як у поштовій скриньці для зберігання своїх електронних листів."</string>
<string name="screen_server_confirmation_title_login">"Ви збираєтесь увійти в %1$s"</string>
<string name="screen_server_confirmation_title_register">"Ви збираєтеся створити обліковий запис на %1$s"</string>
</resources>

View file

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"更改账户提供"</string>
<string name="screen_account_provider_change">"更改账户提供"</string>
<string name="screen_account_provider_form_hint">"服务器地址"</string>
<string name="screen_account_provider_form_notice">"输入搜索词或域名地址。"</string>
<string name="screen_account_provider_form_subtitle">"搜索公司、社区或私人服务器。"</string>
<string name="screen_account_provider_form_title">"查找账户提供者"</string>
<string name="screen_account_provider_signin_subtitle">"这是您的对话将进行的地方,就像您使用电子邮件提供商来保存电子邮件一样。"</string>
<string name="screen_account_provider_signin_title">"您即将登录%s"</string>
<string name="screen_account_provider_signup_subtitle">"这是您的对话将进行的地方,就像您使用电子邮件提供商来保存电子邮件一样。"</string>
<string name="screen_account_provider_form_title">"寻找账户提供方"</string>
<string name="screen_account_provider_signin_subtitle">"这是您的对话将存在的地方,就像您使用电子邮件提供方来保存电子邮件一样。"</string>
<string name="screen_account_provider_signin_title">"您即将登录 %s"</string>
<string name="screen_account_provider_signup_subtitle">"这是您的对话将存在的地方,就像您使用电子邮件提供方来保存电子邮件一样。"</string>
<string name="screen_account_provider_signup_title">"您即将在 %s 上创建一个帐户"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org 由 Matrix.org 基金会运营,是用于安全、去中心化的通信的公共 Matrix 网络上的大型免费服务器。"</string>
<string name="screen_change_account_provider_other">"其他"</string>
<string name="screen_change_account_provider_subtitle">"使用其他帐户提供者,例如您自己的私人服务器或工作帐户。"</string>
<string name="screen_change_account_provider_title">"更改账户提供"</string>
<string name="screen_change_account_provider_subtitle">"使用其他账户提供商,例如您自己的私人服务器或工作账户。"</string>
<string name="screen_change_account_provider_title">"更改账户提供"</string>
<string name="screen_change_server_error_invalid_homeserver">"我们无法访问此服务器。请检查您输入的服务器网址是否正确。如果 URL 正确,请联系您的服务器管理员寻求进一步帮助。"</string>
<string name="screen_change_server_error_invalid_well_known">"由于 Well Known 文件中的问题Sliding Sync 不可用:
%1$s"</string>
@ -21,6 +21,7 @@
<string name="screen_change_server_form_notice">"您只能连接到支持 Sliding Sync 的现有服务器。您的服务器管理员需要对其进行配置。%1$s"</string>
<string name="screen_change_server_subtitle">"您的服务器地址是什么?"</string>
<string name="screen_change_server_title">"选择服务器"</string>
<string name="screen_create_account_title">"创建账户"</string>
<string name="screen_login_error_deactivated_account">"该账户已被停用。"</string>
<string name="screen_login_error_invalid_credentials">"错误的用户名和/或密码"</string>
<string name="screen_login_error_invalid_user_id">"这不是合法的用户 ID。期望格式@user:homeserver.org。"</string>
@ -51,7 +52,7 @@
尝试手动或使用另一个设备扫描二维码."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"不支持二维码"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"账户提供不支持 %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"账户提供不支持 %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"不支持 %1$s."</string>
<string name="screen_qr_code_login_initial_state_button_title">"准备进行扫描"</string>
<string name="screen_qr_code_login_initial_state_item_1">"在桌面设备上打开 %1$s"</string>
@ -59,6 +60,7 @@
<string name="screen_qr_code_login_initial_state_item_3">"选择 %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"「连接新设备」"</string>
<string name="screen_qr_code_login_initial_state_item_4">"使用此设备扫描二维码"</string>
<string name="screen_qr_code_login_initial_state_subtitle">"仅在您的账户提供方支持时才可用。"</string>
<string name="screen_qr_code_login_initial_state_title">"在另一台设备上打开 %1$s 以获取二维码"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"使用其他设备上显示的二维码。"</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"再试一次"</string>
@ -70,12 +72,12 @@
<string name="screen_qr_code_login_start_over_button">"重新开始"</string>
<string name="screen_qr_code_login_unknown_error_description">"发生了意外错误。请再试一次。"</string>
<string name="screen_qr_code_login_verify_code_loading">"等着您的其他设备"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"您的账户提供可能会要求您提供以下代码来验证登录。"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"您的账户提供可能会要求您提供以下代码来验证登录。"</string>
<string name="screen_qr_code_login_verify_code_title">"您的验证码"</string>
<string name="screen_server_confirmation_change_server">"更改账户提供"</string>
<string name="screen_server_confirmation_change_server">"更改账户提供"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"专为 Element 员工提供的私人服务器。"</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix 是一个用于安全、去中心化通信的开放网络。"</string>
<string name="screen_server_confirmation_message_register">"这是您的对话将进行的地方,就像您使用电子邮件提供商来保存电子邮件一样。"</string>
<string name="screen_server_confirmation_message_register">"这是您的对话将存在的地方,就像您使用电子邮件提供方来保存电子邮件一样。"</string>
<string name="screen_server_confirmation_title_login">"即将登录 %1$s"</string>
<string name="screen_server_confirmation_title_register">"即将在 %1$s 上创建一个账户"</string>
</resources>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Haluatko varmasti kirjautua ulos?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Kirjaudu ulos"</string>
<string name="screen_signout_confirmation_dialog_title">"Kirjaudu ulos"</string>
<string name="screen_signout_in_progress_dialog_content">"Kirjaudutaan ulos…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Olet kirjautumassa ulos viimeisestä istunnostasi. Jos kirjaudut ulos nyt, menetät pääsyn salattuihin viesteihisi."</string>
<string name="screen_signout_key_backup_disabled_title">"Olet poistanut varmuuskopioinnin käytöstä"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Avaimiasi varmuuskopioitiin vielä, kun menit offline-tilaan. Muodosta yhteys uudelleen, jotta avaimesi voidaan varmuuskopioida ennen uloskirjautumista."</string>
<string name="screen_signout_key_backup_offline_title">"Avaimiasi varmuuskopioidaan vielä"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Odota, että tämä on valmis ennen uloskirjautumista."</string>
<string name="screen_signout_key_backup_ongoing_title">"Avaimiasi varmuuskopioidaan vielä"</string>
<string name="screen_signout_preference_item">"Kirjaudu ulos"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Olet kirjautumassa ulos viimeisestä istunnostasi. Jos kirjaudut ulos nyt, menetät pääsyn salattuihin viesteihisi."</string>
<string name="screen_signout_recovery_disabled_title">"Palautus ei ole käytössä"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Olet kirjautumassa ulos viimeisestä istunnostasi. Jos kirjaudut ulos nyt, saatat menettää pääsyn salattuihin viesteihisi."</string>
<string name="screen_signout_save_recovery_key_title">"Oletko tallentanut palautusavaimesi?"</string>
</resources>

View file

@ -6,9 +6,9 @@
<string name="screen_signout_in_progress_dialog_content">"Вихід…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Ви збираєтеся вийти зі свого останнього сеансу. Якщо ви вийдете зараз, ви втратите доступ до своїх зашифрованих повідомлень."</string>
<string name="screen_signout_key_backup_disabled_title">"Ви вимкнули резервне копіювання"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Коли ви вийшли з мережі, резервна копія ваших ключів все ще створювалася. Повторно підключіться, щоб зберегти резервну копію ключів перед виходом з системи."</string>
<string name="screen_signout_key_backup_offline_subtitle">"Коли ви вийшли з мережі, резервна копія ваших ключів все ще створювалася. Повторно під\'єднайтеся, щоб зберегти резервну копію ключів перед виходом."</string>
<string name="screen_signout_key_backup_offline_title">"Резервне копіювання ваших ключів ще триває"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Зачекайте, поки це завершиться, перш ніж вийти."</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Дочекайтеся завершення процесу, перш ніж вийти."</string>
<string name="screen_signout_key_backup_ongoing_title">"Резервне копіювання ваших ключів ще триває"</string>
<string name="screen_signout_preference_item">"Вийти"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Ви збираєтеся вийти зі свого останнього сеансу. Якщо ви вийдете зараз, ви втратите доступ до своїх зашифрованих повідомлень."</string>

View file

@ -212,7 +212,7 @@ class MessagesNode @AssistedInject constructor(
state = state,
onBackClick = this::navigateUp,
onRoomDetailsClick = this::onRoomDetailsClick,
onEventClick = this::onEventClick,
onEventContentClick = this::onEventClick,
onPreviewAttachments = this::onPreviewAttachments,
onUserDataClick = this::onUserDataClick,
onLinkClick = { url -> onLinkClick(activity, isDark, url, state.timelineState.eventSink) },

View file

@ -114,7 +114,7 @@ fun MessagesView(
state: MessagesState,
onBackClick: () -> Unit,
onRoomDetailsClick: () -> Unit,
onEventClick: (event: TimelineItem.Event) -> Boolean,
onEventContentClick: (event: TimelineItem.Event) -> Boolean,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
@ -142,9 +142,14 @@ fun MessagesView(
// This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose
val localView = LocalView.current
fun onMessageClick(event: TimelineItem.Event) {
fun hidingKeyboard(block: () -> Unit) {
localView.hideKeyboard()
block()
}
fun onContentClick(event: TimelineItem.Event) {
Timber.v("onMessageClick= ${event.id}")
val hideKeyboard = onEventClick(event)
val hideKeyboard = onEventContentClick(event)
if (hideKeyboard) {
localView.hideKeyboard()
}
@ -152,13 +157,14 @@ fun MessagesView(
fun onMessageLongClick(event: TimelineItem.Event) {
Timber.v("OnMessageLongClicked= ${event.id}")
localView.hideKeyboard()
state.actionListState.eventSink(
ActionListEvents.ComputeForMessage(
event = event,
userEventPermissions = state.userEventPermissions,
hidingKeyboard {
state.actionListState.eventSink(
ActionListEvents.ComputeForMessage(
event = event,
userEventPermissions = state.userEventPermissions,
)
)
)
}
}
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
@ -189,13 +195,8 @@ fun MessagesView(
roomAvatar = state.roomAvatar.dataOrNull(),
heroes = state.heroes,
roomCallState = state.roomCallState,
onBackClick = {
// Since the textfield is now based on an Android view, this is no longer done automatically.
// We need to hide the keyboard when navigating out of this screen.
localView.hideKeyboard()
onBackClick()
},
onRoomDetailsClick = onRoomDetailsClick,
onBackClick = { hidingKeyboard { onBackClick() } },
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
onJoinCallClick = onJoinCallClick,
)
}
@ -206,9 +207,9 @@ fun MessagesView(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding),
onMessageClick = ::onMessageClick,
onContentClick = ::onContentClick,
onMessageLongClick = ::onMessageLongClick,
onUserDataClick = onUserDataClick,
onUserDataClick = { hidingKeyboard { onUserDataClick(it) } },
onLinkClick = onLinkClick,
onReactionClick = ::onEmojiReactionClick,
onReactionLongClick = ::onEmojiReactionLongClick,
@ -306,7 +307,7 @@ private fun AttachmentStateView(
@Composable
private fun MessagesViewContent(
state: MessagesState,
onMessageClick: (TimelineItem.Event) -> Unit,
onContentClick: (TimelineItem.Event) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
@ -382,7 +383,7 @@ private fun MessagesViewContent(
timelineProtectionState = state.timelineProtectionState,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onMessageClick = onMessageClick,
onContentClick = onContentClick,
onMessageLongClick = onMessageLongClick,
onSwipeToReply = onSwipeToReply,
onReactionClick = onReactionClick,
@ -568,7 +569,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
state = state,
onBackClick = {},
onRoomDetailsClick = {},
onEventClick = { false },
onEventContentClick = { false },
onUserDataClick = {},
onLinkClick = {},
onPreviewAttachments = {},

View file

@ -33,7 +33,7 @@ internal fun MessagesViewWithIdentityChangePreview(
),
onBackClick = {},
onRoomDetailsClick = {},
onEventClick = { false },
onEventContentClick = { false },
onUserDataClick = {},
onLinkClick = {},
onPreviewAttachments = {},

View file

@ -51,10 +51,12 @@ class ResolveVerifiedUserSendFailurePresenter @Inject constructor(
is ResolveVerifiedUserSendFailureEvents.ComputeForMessage -> {
val sendState = event.messageEvent.localSendState as? LocalEventSendState.Failed.VerifiedUser
val transactionId = event.messageEvent.transactionId
resolver = if (sendState != null && transactionId != null) {
val sendHandle = event.messageEvent.sendhandle
resolver = if (sendState != null && transactionId != null && sendHandle != null) {
VerifiedUserSendFailureResolver(
room = room,
transactionId = transactionId,
sendHandle = sendHandle,
iterator = VerifiedUserSendFailureIterator.from(sendState)
)
} else {

View file

@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.crypto.sendfailure.resolve
import androidx.compose.runtime.mutableStateOf
import io.element.android.libraries.matrix.api.core.SendHandle
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
@ -22,6 +23,7 @@ import timber.log.Timber
class VerifiedUserSendFailureResolver(
private val room: MatrixRoom,
private val transactionId: TransactionId,
private val sendHandle: SendHandle,
private val iterator: VerifiedUserSendFailureIterator,
) {
val currentSendFailure = mutableStateOf<LocalEventSendState.Failed.VerifiedUser?>(null)
@ -33,7 +35,7 @@ class VerifiedUserSendFailureResolver(
}
suspend fun resend(): Result<Unit> {
return room.retrySendMessage(transactionId)
return sendHandle.retry()
.onSuccess {
Timber.d("Succeed to resend message with transactionId: $transactionId")
currentSendFailure.value = null
@ -46,10 +48,10 @@ class VerifiedUserSendFailureResolver(
suspend fun resolveAndResend(): Result<Unit> {
return when (val failure = currentSendFailure.value) {
is LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice -> {
room.ignoreDeviceTrustAndResend(failure.devices, transactionId)
room.ignoreDeviceTrustAndResend(failure.devices, sendHandle)
}
is LocalEventSendState.Failed.VerifiedUserChangedIdentity -> {
room.withdrawVerificationAndResend(failure.users, transactionId)
room.withdrawVerificationAndResend(failure.users, sendHandle)
}
else -> {
Result.failure(IllegalStateException("Unknown send failure type"))

View file

@ -216,7 +216,7 @@ private fun PinnedMessagesListLoaded(
focusedEventId = null,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onClick = onEventClick,
onContentClick = onEventClick,
onLongClick = ::onMessageLongClick,
inReplyToClick = {},
onReactionClick = { _, _ -> },
@ -230,6 +230,7 @@ private fun PinnedMessagesListLoaded(
TimelineItemEventContentViewWrapper(
event = event,
timelineProtectionState = state.timelineProtectionState,
onContentClick = { onEventClick(event) },
onLinkClick = onLinkClick,
modifier = contentModifier,
onContentLayoutChange = onContentLayoutChange
@ -244,6 +245,7 @@ private fun PinnedMessagesListLoaded(
private fun TimelineItemEventContentViewWrapper(
event: TimelineItem.Event,
timelineProtectionState: TimelineProtectionState,
onContentClick: () -> Unit,
onLinkClick: (String) -> Unit,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
modifier: Modifier = Modifier,
@ -258,10 +260,12 @@ private fun TimelineItemEventContentViewWrapper(
TimelineItemEventContentView(
content = event.content,
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick,
eventSink = { },
modifier = modifier,
onContentClick = onContentClick,
onLongClick = null,
onContentLayoutChange = onContentLayoutChange
)
}

View file

@ -27,7 +27,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
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
@ -39,9 +38,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings
@ -89,21 +88,16 @@ fun ReportMessageView(
) {
Spacer(modifier = Modifier.height(20.dp))
OutlinedTextField(
TextField(
value = state.reason,
onValueChange = { state.eventSink(ReportMessageEvents.UpdateReason(it)) },
placeholder = { Text(stringResource(R.string.screen_report_content_hint)) },
placeholder = stringResource(R.string.screen_report_content_hint),
minLines = 3,
enabled = !isSending,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 90.dp)
)
Text(
text = stringResource(R.string.screen_report_content_explanation),
style = ElementTheme.typography.fontBodySmRegular,
color = MaterialTheme.colorScheme.secondary,
textAlign = TextAlign.Start,
modifier = Modifier.padding(top = 4.dp, bottom = 24.dp, start = 16.dp, end = 16.dp)
.heightIn(min = 90.dp),
supportingText = stringResource(R.string.screen_report_content_explanation),
)
Row(

View file

@ -169,6 +169,7 @@ internal fun aTimelineItemEvent(
origin = null,
timelineItemDebugInfoProvider = { debugInfo },
messageShieldProvider = { messageShield },
sendHandleProvider = { null }
)
}

View file

@ -76,7 +76,7 @@ fun TimelineView(
timelineProtectionState: TimelineProtectionState,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onMessageClick: (TimelineItem.Event) -> Unit,
onContentClick: (TimelineItem.Event) -> Unit,
onMessageLongClick: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
onReactionClick: (emoji: String, TimelineItem.Event) -> Unit,
@ -141,7 +141,7 @@ fun TimelineView(
focusedEventId = state.focusedEventId,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onClick = onMessageClick,
onContentClick = onContentClick,
onLongClick = onMessageLongClick,
inReplyToClick = ::inReplyToClick,
onReactionClick = onReactionClick,
@ -322,7 +322,7 @@ internal fun TimelineViewPreview(
timelineProtectionState = aTimelineProtectionState(),
onUserDataClick = {},
onLinkClick = {},
onMessageClick = {},
onContentClick = {},
onMessageLongClick = {},
onSwipeToReply = {},
onReactionClick = { _, _ -> },

View file

@ -41,7 +41,7 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview {
timelineProtectionState = aTimelineProtectionState(),
onUserDataClick = {},
onLinkClick = {},
onMessageClick = {},
onContentClick = {},
onMessageLongClick = {},
onSwipeToReply = {},
onReactionClick = { _, _ -> },

View file

@ -30,7 +30,7 @@ internal fun ATimelineItemEventRow(
timelineProtectionState = timelineProtectionState,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = isHighlighted,
onClick = {},
onEventClick = {},
onLongClick = {},
onLinkClick = {},
onUserDataClick = {},

View file

@ -114,7 +114,7 @@ fun TimelineItemEventRow(
renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean,
isHighlighted: Boolean,
onClick: () -> Unit,
onEventClick: () -> Unit,
onLongClick: () -> Unit,
onLinkClick: (String) -> Unit,
onUserDataClick: (UserId) -> Unit,
@ -127,10 +127,15 @@ fun TimelineItemEventRow(
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
modifier: Modifier = Modifier,
eventContentView: @Composable (Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = { contentModifier, onContentLayoutChange ->
// Only pass down a custom clickable lambda if the content can be clicked separately
val onContentClick = onEventClick.takeUnless { event.isWholeContentClickable }
TimelineItemEventContentView(
content = event.content,
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onContentClick = onContentClick,
onLongClick = onLongClick,
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick,
eventSink = eventSink,
modifier = contentModifier,
@ -173,7 +178,7 @@ fun TimelineItemEventRow(
isHighlighted = isHighlighted,
timelineRoomInfo = timelineRoomInfo,
interactionSource = interactionSource,
onClick = onClick,
onContentClick = onEventClick,
onLongClick = onLongClick,
inReplyToClick = ::inReplyToClick,
onUserDataClick = ::onUserDataClick,
@ -207,7 +212,7 @@ fun TimelineItemEventRow(
isHighlighted = isHighlighted,
timelineRoomInfo = timelineRoomInfo,
interactionSource = interactionSource,
onClick = onClick,
onContentClick = onEventClick,
onLongClick = onLongClick,
inReplyToClick = ::inReplyToClick,
onUserDataClick = ::onUserDataClick,
@ -263,7 +268,7 @@ private fun TimelineItemEventRowContent(
isHighlighted: Boolean,
timelineRoomInfo: TimelineRoomInfo,
interactionSource: MutableInteractionSource,
onClick: () -> Unit,
onContentClick: () -> Unit,
onLongClick: () -> Unit,
inReplyToClick: () -> Unit,
onUserDataClick: () -> Unit,
@ -340,7 +345,7 @@ private fun TimelineItemEventRowContent(
},
state = bubbleState,
interactionSource = interactionSource,
onClick = onClick,
onClick = onContentClick,
onLongClick = onLongClick,
) {
MessageEventBubbleContent(

View file

@ -57,10 +57,12 @@ fun TimelineItemGroupedEventsRow(
TimelineItemEventContentView(
content = event.content,
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick,
eventSink = eventSink,
modifier = contentModifier,
onContentClick = null,
onLongClick = null,
onContentLayoutChange = onContentLayoutChange
)
},
@ -121,10 +123,12 @@ private fun TimelineItemGroupedEventsRowContent(
TimelineItemEventContentView(
content = event.content,
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick,
eventSink = eventSink,
modifier = contentModifier,
onContentClick = null,
onLongClick = null,
onContentLayoutChange = onContentLayoutChange
)
},
@ -152,7 +156,7 @@ private fun TimelineItemGroupedEventsRowContent(
focusedEventId = focusedEventId,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onClick = onClick,
onContentClick = onClick,
onLongClick = onLongClick,
inReplyToClick = inReplyToClick,
onReactionClick = onReactionClick,

View file

@ -28,7 +28,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
import io.element.android.features.messages.impl.timeline.protection.mustBeProtected
import io.element.android.libraries.designsystem.text.toPx
import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor
import io.element.android.libraries.matrix.api.core.EventId
@ -44,7 +43,7 @@ internal fun TimelineItemRow(
focusedEventId: EventId?,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onClick: (TimelineItem.Event) -> Unit,
onContentClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit,
inReplyToClick: (EventId) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
@ -60,7 +59,9 @@ internal fun TimelineItemRow(
TimelineItemEventContentView(
content = event.content,
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onContentClick = { onContentClick(event) },
onLongClick = { onLongClick(event) },
onLinkClick = onLinkClick,
eventSink = eventSink,
modifier = contentModifier,
@ -95,7 +96,7 @@ internal fun TimelineItemRow(
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = timelineItem.isEvent(focusedEventId),
onClick = { onClick(timelineItem) },
onClick = { onContentClick(timelineItem) },
onReadReceiptsClick = onReadReceiptClick,
onLongClick = { onLongClick(timelineItem) },
eventSink = eventSink,
@ -118,11 +119,7 @@ internal fun TimelineItemRow(
timelineProtectionState = timelineProtectionState,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = timelineItem.isEvent(focusedEventId),
onClick = if (timelineProtectionState.hideMediaContent(timelineItem.eventId) && timelineItem.mustBeProtected()) {
{}
} else {
{ onClick(timelineItem) }
},
onEventClick = { onContentClick(timelineItem) },
onLongClick = { onLongClick(timelineItem) },
onLinkClick = onLinkClick,
onUserDataClick = onUserDataClick,
@ -148,7 +145,7 @@ internal fun TimelineItemRow(
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
focusedEventId = focusedEventId,
onClick = onClick,
onClick = onContentClick,
onLongClick = onLongClick,
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,

View file

@ -72,8 +72,10 @@ fun TimelineItemStateEventRow(
content = event.content,
onLinkClick = {},
hideMediaContent = false,
onShowClick = {},
onShowContentClick = {},
eventSink = eventSink,
onContentClick = null,
onLongClick = null,
modifier = Modifier.defaultTimelineContentPadding()
)
}

View file

@ -36,7 +36,9 @@ import io.element.android.libraries.architecture.Presenter
fun TimelineItemEventContentView(
content: TimelineItemEventContent,
hideMediaContent: Boolean,
onShowClick: () -> Unit,
onContentClick: (() -> Unit)?,
onLongClick: (() -> Unit)?,
onShowContentClick: () -> Unit,
onLinkClick: (url: String) -> Unit,
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
modifier: Modifier = Modifier,
@ -72,20 +74,28 @@ fun TimelineItemEventContentView(
is TimelineItemImageContent -> TimelineItemImageView(
content = content,
hideMediaContent = hideMediaContent,
onShowClick = onShowClick,
onContentClick = onContentClick,
onLongClick = onLongClick,
onShowContentClick = onShowContentClick,
onLinkClick = onLinkClick,
onContentLayoutChange = onContentLayoutChange,
modifier = modifier,
)
is TimelineItemStickerContent -> TimelineItemStickerView(
content = content,
hideMediaContent = hideMediaContent,
onShowClick = onShowClick,
onContentClick = onContentClick,
onLongClick = onLongClick,
onShowClick = onShowContentClick,
modifier = modifier,
)
is TimelineItemVideoContent -> TimelineItemVideoView(
content = content,
hideMediaContent = hideMediaContent,
onShowClick = onShowClick,
onContentClick = onContentClick,
onLongClick = onLongClick,
onShowContentClick = onShowContentClick,
onLinkClick = onLinkClick,
onContentLayoutChange = onContentLayoutChange,
modifier = modifier
)

View file

@ -8,7 +8,9 @@
package io.element.android.features.messages.impl.timeline.components.event
import android.text.SpannedString
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -50,16 +52,19 @@ import io.element.android.features.messages.impl.timeline.protection.ProtectedVi
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.wysiwyg.compose.EditorStyledText
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TimelineItemImageView(
content: TimelineItemImageContent,
hideMediaContent: Boolean,
onShowClick: () -> Unit,
onContentClick: (() -> Unit)?,
onLongClick: (() -> Unit)?,
onLinkClick: (String) -> Unit,
onShowContentClick: () -> Unit,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
modifier: Modifier = Modifier,
) {
@ -78,13 +83,14 @@ fun TimelineItemImageView(
) {
ProtectedView(
hideContent = hideMediaContent,
onShowClick = onShowClick,
onShowClick = onShowContentClick,
) {
var isLoaded by remember { mutableStateOf(false) }
AsyncImage(
modifier = Modifier
.fillMaxWidth()
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
.then(if (isLoaded) Modifier.background(Color.White) else Modifier)
.then(if (onContentClick != null) Modifier.combinedClickable(onClick = onContentClick, onLongClick = onLongClick) else Modifier),
model = content.thumbnailMediaRequestData,
contentScale = ContentScale.Fit,
alignment = Alignment.Center,
@ -99,9 +105,7 @@ fun TimelineItemImageView(
val caption = if (LocalInspectionMode.current) {
SpannedString(content.caption)
} else {
content.formattedCaption?.body
?.takeIf { content.formattedCaption.format == MessageFormat.HTML }
?: SpannedString(content.caption)
content.formattedCaption ?: SpannedString(content.caption)
}
CompositionLocalProvider(
LocalContentColor provides ElementTheme.colors.textPrimary,
@ -114,6 +118,7 @@ fun TimelineItemImageView(
.widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio),
text = caption,
style = ElementRichTextEditorStyle.textStyle(),
onLinkClickedListener = onLinkClick,
releaseOnDetach = false,
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange),
)
@ -128,7 +133,10 @@ internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageCon
TimelineItemImageView(
content = content,
hideMediaContent = false,
onShowClick = {},
onShowContentClick = {},
onContentClick = {},
onLongClick = {},
onLinkClick = {},
onContentLayoutChange = {},
)
}
@ -139,7 +147,10 @@ internal fun TimelineItemImageViewHideMediaContentPreview() = ElementPreview {
TimelineItemImageView(
content = aTimelineItemImageContent(),
hideMediaContent = true,
onShowClick = {},
onShowContentClick = {},
onContentClick = {},
onLongClick = {},
onLinkClick = {},
onContentLayoutChange = {},
)
}

View file

@ -7,24 +7,21 @@
package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
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.messages.impl.R
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.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun TimelineItemLegacyCallInviteView(
@ -32,20 +29,18 @@ fun TimelineItemLegacyCallInviteView(
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = CompoundIcons.VoiceCall(),
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
tint = ElementTheme.colors.iconSecondary,
)
Spacer(modifier = Modifier.width(8.dp))
Text(
color = MaterialTheme.colorScheme.secondary,
color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodyMdRegular,
text = stringResource(CommonStrings.common_call_invite),
textAlign = TextAlign.Center,
text = stringResource(R.string.screen_room_timeline_legacy_call),
textAlign = TextAlign.Start,
)
}
}

View file

@ -51,5 +51,7 @@ fun TimelineItemLocationView(
@Composable
internal fun TimelineItemLocationViewPreview(@PreviewParameter(TimelineItemLocationContentProvider::class) content: TimelineItemLocationContent) =
ElementPreview {
TimelineItemLocationView(content)
TimelineItemLocationView(
content = content,
)
}

View file

@ -7,7 +7,9 @@
package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
@ -36,10 +38,13 @@ import io.element.android.libraries.ui.strings.CommonStrings
private const val STICKER_SIZE_IN_DP = 128
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TimelineItemStickerView(
content: TimelineItemStickerContent,
hideMediaContent: Boolean,
onContentClick: (() -> Unit)?,
onLongClick: (() -> Unit)?,
onShowClick: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -61,7 +66,8 @@ fun TimelineItemStickerView(
AsyncImage(
modifier = Modifier
.fillMaxSize()
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
.then(if (isLoaded) Modifier.background(Color.White) else Modifier)
.then(if (onContentClick != null) Modifier.combinedClickable(onClick = onContentClick, onLongClick = onLongClick) else Modifier),
model = MediaRequestData(
source = content.preferredMediaSource,
kind = MediaRequestData.Kind.File(
@ -85,6 +91,8 @@ internal fun TimelineItemStickerViewPreview(@PreviewParameter(TimelineItemSticke
TimelineItemStickerView(
content = content,
hideMediaContent = false,
onContentClick = {},
onLongClick = {},
onShowClick = {},
)
}

View file

@ -8,8 +8,10 @@
package io.element.android.features.messages.impl.timeline.components.event
import android.text.SpannedString
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@ -56,7 +58,6 @@ import io.element.android.libraries.designsystem.components.blurhash.blurHashBac
import io.element.android.libraries.designsystem.modifiers.roundedBackground
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_HEIGHT
import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH
import io.element.android.libraries.matrix.ui.media.MediaRequestData
@ -64,11 +65,15 @@ import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.wysiwyg.compose.EditorStyledText
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TimelineItemVideoView(
content: TimelineItemVideoContent,
hideMediaContent: Boolean,
onShowClick: () -> Unit,
onContentClick: (() -> Unit)?,
onLongClick: (() -> Unit)?,
onShowContentClick: () -> Unit,
onLinkClick: (String) -> Unit,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
modifier: Modifier = Modifier,
) {
@ -90,13 +95,14 @@ fun TimelineItemVideoView(
) {
ProtectedView(
hideContent = hideMediaContent,
onShowClick = onShowClick,
onShowClick = onShowContentClick,
) {
var isLoaded by remember { mutableStateOf(false) }
AsyncImage(
modifier = Modifier
.fillMaxWidth()
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
.then(if (isLoaded) Modifier.background(Color.White) else Modifier)
.then(if (onContentClick != null) Modifier.combinedClickable(onClick = onContentClick, onLongClick = onLongClick) else Modifier),
model = MediaRequestData(
source = content.thumbnailSource,
kind = MediaRequestData.Kind.Thumbnail(
@ -128,9 +134,7 @@ fun TimelineItemVideoView(
val caption = if (LocalInspectionMode.current) {
SpannedString(content.caption)
} else {
content.formattedCaption?.body
?.takeIf { content.formattedCaption.format == MessageFormat.HTML }
?: SpannedString(content.caption)
content.formattedCaption ?: SpannedString(content.caption)
}
CompositionLocalProvider(
LocalContentColor provides ElementTheme.colors.textPrimary,
@ -142,6 +146,7 @@ fun TimelineItemVideoView(
.padding(horizontal = 4.dp) // This is (12.dp - 8.dp) contentPadding from CommonLayout
.widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio),
text = caption,
onLinkClickedListener = onLinkClick,
style = ElementRichTextEditorStyle.textStyle(),
releaseOnDetach = false,
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange),
@ -157,7 +162,10 @@ internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoCon
TimelineItemVideoView(
content = content,
hideMediaContent = false,
onShowClick = {},
onShowContentClick = {},
onContentClick = {},
onLongClick = {},
onLinkClick = {},
onContentLayoutChange = {},
)
}
@ -168,7 +176,10 @@ internal fun TimelineItemVideoViewHideMediaContentPreview() = ElementPreview {
TimelineItemVideoView(
content = aTimelineItemVideoContent(),
hideMediaContent = true,
onShowClick = {},
onShowContentClick = {},
onContentClick = {},
onLongClick = {},
onLinkClick = {},
onContentLayoutChange = {},
)
}

View file

@ -86,7 +86,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
TimelineItemImageContent(
filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
mediaSource = messageType.source,
thumbnailSource = messageType.info?.thumbnailSource,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -105,7 +105,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
TimelineItemStickerContent(
filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
mediaSource = messageType.source,
thumbnailSource = messageType.info?.thumbnailSource,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -142,7 +142,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
TimelineItemVideoContent(
filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
thumbnailSource = messageType.info?.thumbnailSource,
videoSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -161,7 +161,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
TimelineItemAudioContent(
filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -176,7 +176,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
eventId = eventId,
filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -187,7 +187,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
TimelineItemAudioContent(
filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -202,7 +202,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
TimelineItemFileContent(
filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
thumbnailSource = messageType.info?.thumbnailSource,
fileSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),

View file

@ -88,6 +88,7 @@ class TimelineItemEventFactory @AssistedInject constructor(
origin = currentTimelineItem.event.origin,
timelineItemDebugInfoProvider = currentTimelineItem.event.timelineItemDebugInfoProvider,
messageShieldProvider = currentTimelineItem.event.messageShieldProvider,
sendHandleProvider = currentTimelineItem.event.sendHandleProvider,
)
}

View file

@ -9,11 +9,14 @@ package io.element.android.features.messages.impl.timeline.model
import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.SendHandle
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.core.UserId
@ -23,6 +26,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSen
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShieldProvider
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.SendHandleProvider
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemDebugInfoProvider
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
@ -80,6 +84,7 @@ sealed interface TimelineItem {
val origin: TimelineItemEventOrigin?,
val timelineItemDebugInfoProvider: TimelineItemDebugInfoProvider,
val messageShieldProvider: MessageShieldProvider,
val sendHandleProvider: SendHandleProvider,
) : TimelineItem {
val showSenderInformation = groupPosition.isNew() && !isMine
@ -93,6 +98,17 @@ sealed interface TimelineItem {
val isRemote = eventId != null
/** Whether a click on any part of the event bubble should trigger the 'onContentClick' callback.
*
* This is `true` for all events except for visual media events with a caption or formatted caption.
*/
val isWholeContentClickable = when (content) {
is TimelineItemStickerContent -> content.formattedCaption == null && content.caption == null
is TimelineItemImageContent -> content.formattedCaption == null && content.caption == null
is TimelineItemVideoContent -> content.formattedCaption == null && content.caption == null
else -> true
}
val eventOrTransactionId: EventOrTransactionId
get() = EventOrTransactionId.from(eventId = eventId, transactionId = transactionId)
@ -101,6 +117,8 @@ sealed interface TimelineItem {
val debugInfo: TimelineItemDebugInfo
get() = timelineItemDebugInfoProvider()
val sendhandle: SendHandle? get() = sendHandleProvider()
}
@Immutable

View file

@ -8,14 +8,13 @@
package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
import kotlin.time.Duration
data class TimelineItemAudioContent(
override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
override val formattedCaption: CharSequence?,
val duration: Duration,
val mediaSource: MediaSource,
val mimeType: String,

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