Merge remote-tracking branch 'origin/develop' into feature/bma/protectLinkNewDevice
This commit is contained in:
@@ -27,7 +27,7 @@
|
||||
<string name="state_event_room_invite_accepted">"%1$s принял(а) приглашение"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Вы приняли приглашение"</string>
|
||||
<string name="state_event_room_invite_by_you">"Вы пригласили %1$s"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s пригласил(а) вас"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s пригласил(а) Вас"</string>
|
||||
<string name="state_event_room_join">"%1$s присоединился(ась)"</string>
|
||||
<string name="state_event_room_join_by_you">"Вы присоединились к комнате"</string>
|
||||
<string name="state_event_room_knock">"%1$s хочет присоединиться"</string>
|
||||
@@ -47,8 +47,8 @@
|
||||
<string name="state_event_room_name_removed_by_you">"Вы удалили название"</string>
|
||||
<string name="state_event_room_none">"%1$s не внес(ла) изменений"</string>
|
||||
<string name="state_event_room_none_by_you">"Вы не внесли изменений"</string>
|
||||
<string name="state_event_room_pinned_events_changed">"%1$s изменил(а) закрепленные сообщения"</string>
|
||||
<string name="state_event_room_pinned_events_changed_by_you">"Вы изменили закрепленные сообщения"</string>
|
||||
<string name="state_event_room_pinned_events_changed">"%1$s изменил(а) закреплённые сообщения"</string>
|
||||
<string name="state_event_room_pinned_events_changed_by_you">"Вы изменили закреплённые сообщения"</string>
|
||||
<string name="state_event_room_pinned_events_pinned">"%1$s закрепил(а) сообщение"</string>
|
||||
<string name="state_event_room_pinned_events_pinned_by_you">"Вы закрепили сообщение"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned">"%1$s открепил(а) сообщение"</string>
|
||||
|
||||
+2
@@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.MediaPreviewService
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||
import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
@@ -53,6 +54,7 @@ import java.util.Optional
|
||||
interface MatrixClient {
|
||||
val sessionId: SessionId
|
||||
val deviceId: DeviceId
|
||||
val sessionPaths: SessionPaths
|
||||
val userProfile: StateFlow<MatrixUser>
|
||||
val roomListService: RoomListService
|
||||
val spaceService: SpaceService
|
||||
|
||||
+1
-1
@@ -10,5 +10,5 @@ package io.element.android.libraries.matrix.api.analytics
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
fun interface GetDatabaseSizesUseCase {
|
||||
suspend operator fun invoke(sessionId: SessionId): Result<SdkStoreSizes>
|
||||
operator fun invoke(sessionId: SessionId): Result<SdkStoreSizes>
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.paths
|
||||
|
||||
import java.io.File
|
||||
|
||||
data class SessionPaths(
|
||||
val fileDirectory: File,
|
||||
val cacheDirectory: File,
|
||||
) {
|
||||
fun deleteRecursively() {
|
||||
fileDirectory.deleteRecursively()
|
||||
cacheDirectory.deleteRecursively()
|
||||
}
|
||||
}
|
||||
+6
-8
@@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
@@ -81,7 +82,6 @@ import io.element.android.libraries.matrix.impl.spaces.RustSpaceService
|
||||
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
||||
import io.element.android.libraries.matrix.impl.sync.map
|
||||
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
|
||||
import io.element.android.libraries.matrix.impl.util.SessionPathsProvider
|
||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
|
||||
@@ -137,6 +137,7 @@ import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset
|
||||
import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
|
||||
|
||||
class RustMatrixClient(
|
||||
override val sessionPaths: SessionPaths,
|
||||
private val innerClient: Client,
|
||||
private val sessionStore: SessionStore,
|
||||
private val sessionDelegate: RustClientSessionDelegate,
|
||||
@@ -187,8 +188,6 @@ class RustMatrixClient(
|
||||
sessionDispatcher = sessionDispatcher,
|
||||
)
|
||||
|
||||
private val sessionPathsProvider = SessionPathsProvider(sessionStore)
|
||||
|
||||
private val roomSyncSubscriber: RoomSyncSubscriber = RoomSyncSubscriber(innerRoomListService, dispatchers)
|
||||
|
||||
override val roomListService: RoomListService = RustRoomListService(
|
||||
@@ -824,17 +823,16 @@ class RustMatrixClient(
|
||||
private suspend fun getCacheSize(
|
||||
includeCryptoDb: Boolean = false,
|
||||
): Long = withContext(sessionDispatcher) {
|
||||
val sessionDirectory = sessionPathsProvider.provides(sessionId) ?: return@withContext 0L
|
||||
val cacheSize = sessionDirectory.cacheDirectory.getSizeOfFiles()
|
||||
val cacheSize = sessionPaths.cacheDirectory.getSizeOfFiles()
|
||||
if (includeCryptoDb) {
|
||||
cacheSize + sessionDirectory.fileDirectory.getSizeOfFiles()
|
||||
cacheSize + sessionPaths.fileDirectory.getSizeOfFiles()
|
||||
} else {
|
||||
cacheSize + listOf(
|
||||
"matrix-sdk-state.sqlite3",
|
||||
"matrix-sdk-state.sqlite3-shm",
|
||||
"matrix-sdk-state.sqlite3-wal",
|
||||
).map { fileName ->
|
||||
File(sessionDirectory.fileDirectory, fileName)
|
||||
File(sessionPaths.fileDirectory, fileName)
|
||||
}.sumOf { file ->
|
||||
file.length()
|
||||
}
|
||||
@@ -843,7 +841,7 @@ class RustMatrixClient(
|
||||
|
||||
private suspend fun deleteSessionDirectory() = withContext(sessionDispatcher) {
|
||||
// Delete all the files for this session
|
||||
sessionPathsProvider.provides(sessionId)?.deleteRecursively()
|
||||
sessionPaths.deleteRecursively()
|
||||
}
|
||||
|
||||
private fun scheduleDatabaseVacuum() {
|
||||
|
||||
+4
-3
@@ -17,9 +17,9 @@ import io.element.android.libraries.di.CacheDirectory
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
|
||||
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
|
||||
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
|
||||
import io.element.android.libraries.matrix.impl.room.TimelineEventFilterFactory
|
||||
@@ -102,10 +102,10 @@ class RustMatrixClientFactory(
|
||||
|
||||
client.restoreSession(sessionData.toSession())
|
||||
|
||||
create(client)
|
||||
create(client, sessionData)
|
||||
}
|
||||
|
||||
suspend fun create(client: Client): RustMatrixClient {
|
||||
suspend fun create(client: Client, sessionData: SessionData): RustMatrixClient {
|
||||
val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()
|
||||
|
||||
// Must be called before creating the sync service, timelines etc.
|
||||
@@ -121,6 +121,7 @@ class RustMatrixClientFactory(
|
||||
.finish()
|
||||
|
||||
return RustMatrixClient(
|
||||
sessionPaths = sessionData.getSessionPaths(),
|
||||
innerClient = client,
|
||||
sessionStore = sessionStore,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
|
||||
+37
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
@@ -9,19 +9,53 @@ package io.element.android.libraries.matrix.impl.analytics
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.data.bytes
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
|
||||
import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* An implementation of [GetDatabaseSizesUseCase] that manually calculates the database sizes by checking the file system.
|
||||
* This is not as accurate as the SDK's internal method, but is used as a fallback for Sentry's non-suspendable hook where
|
||||
* calling the SDK's suspendable method blocks the main thread and causes ANRs.
|
||||
*/
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultGetDatabaseSizesUseCase(
|
||||
private val clientProvider: Lazy<MatrixClientProvider>,
|
||||
) : GetDatabaseSizesUseCase {
|
||||
override suspend fun invoke(sessionId: SessionId): Result<SdkStoreSizes> {
|
||||
override fun invoke(sessionId: SessionId): Result<SdkStoreSizes> {
|
||||
val client = clientProvider.value.getOrNull(sessionId)
|
||||
?: return Result.failure(IllegalArgumentException("No MatrixClient for session $sessionId"))
|
||||
|
||||
return client.getDatabaseSizes()
|
||||
val fileDir = client.sessionPaths.fileDirectory
|
||||
val cacheDir = client.sessionPaths.cacheDirectory
|
||||
|
||||
val cryptoSize = getDatabaseSize(fileDir, "matrix-sdk-crypto")
|
||||
val stateSize = getDatabaseSize(fileDir, "matrix-sdk-state")
|
||||
|
||||
val eventCacheSize = getDatabaseSize(cacheDir, "matrix-sdk-event-cache")
|
||||
val mediaCacheSize = getDatabaseSize(cacheDir, "matrix-sdk-media")
|
||||
|
||||
return Result.success(SdkStoreSizes(
|
||||
stateStore = stateSize.bytes,
|
||||
cryptoStore = cryptoSize.bytes,
|
||||
eventCacheStore = eventCacheSize.bytes,
|
||||
mediaStore = mediaCacheSize.bytes,
|
||||
))
|
||||
}
|
||||
|
||||
private fun getDatabaseSize(baseDirectory: File, databaseFileName: String): Long {
|
||||
// Check the sizes of both the main database file and the -wal file, and return the max of the two:
|
||||
// The -wal file can contain additional data not yet merged into the main file, becoming larger than it.
|
||||
val databaseFile = File(baseDirectory, "$databaseFileName.sqlite3")
|
||||
val walFile = File(baseDirectory, "$databaseFileName.sqlite3-wal")
|
||||
|
||||
val databaseSize = if (databaseFile.exists()) databaseFile.length() else 0L
|
||||
val walSize = if (walFile.exists()) walFile.length() else 0L
|
||||
|
||||
return max(databaseSize, walSize)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDat
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
|
||||
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
|
||||
@@ -38,7 +39,6 @@ import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
|
||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import io.element.android.libraries.matrix.impl.keys.SecretGenerator
|
||||
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||
import io.element.android.libraries.matrix.impl.toSession
|
||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
@@ -160,7 +160,7 @@ class RustMatrixAuthenticationService(
|
||||
passphrase = pendingKey.formattedAsString(),
|
||||
sessionPaths = currentSessionPaths,
|
||||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData)
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
@@ -235,7 +235,7 @@ class RustMatrixAuthenticationService(
|
||||
|
||||
// We restore the client using the just retrieved session data
|
||||
client.restoreSession(sessionData.toSession())
|
||||
val matrixClient = rustMatrixClientFactory.create(client)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData)
|
||||
|
||||
// We wait for the verification state to be known
|
||||
matrixClient.waitForKnownVerificationState()
|
||||
@@ -325,7 +325,7 @@ class RustMatrixAuthenticationService(
|
||||
passphrase = pendingKey.formattedAsString(),
|
||||
sessionPaths = currentSessionPaths,
|
||||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData)
|
||||
matrixClient.waitForKnownVerificationState()
|
||||
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
@@ -390,7 +390,7 @@ class RustMatrixAuthenticationService(
|
||||
passphrase = pendingKey.formattedAsString(),
|
||||
sessionPaths = emptySessionPaths,
|
||||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData)
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
package io.element.android.libraries.matrix.impl.mapper
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import org.matrix.rustcomponents.sdk.Session
|
||||
|
||||
+1
-10
@@ -8,19 +8,10 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.paths
|
||||
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import java.io.File
|
||||
|
||||
data class SessionPaths(
|
||||
val fileDirectory: File,
|
||||
val cacheDirectory: File,
|
||||
) {
|
||||
fun deleteRecursively() {
|
||||
fileDirectory.deleteRecursively()
|
||||
cacheDirectory.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun SessionData.getSessionPaths(): SessionPaths {
|
||||
return SessionPaths(
|
||||
fileDirectory = File(sessionPath),
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.paths
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.di.BaseDirectory
|
||||
import io.element.android.libraries.di.CacheDirectory
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
+8
-1
@@ -8,6 +8,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import io.element.android.appconfig.TimelineConfig
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.coroutine.childScope
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
@@ -222,7 +223,13 @@ class JoinedRustRoom(
|
||||
)
|
||||
is CreateTimelineParams.Focused,
|
||||
CreateTimelineParams.PinnedOnly,
|
||||
is CreateTimelineParams.Threaded -> TimelineFilter.All
|
||||
is CreateTimelineParams.Threaded -> {
|
||||
RustTimelineEventFilterFactory().create(
|
||||
joinRule = roomInfoFlow.value.joinRule,
|
||||
isEncrypted = roomInfoFlow.value.isEncrypted,
|
||||
excludedStateTypes = TimelineConfig.excludedEvents,
|
||||
)?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All
|
||||
}
|
||||
}
|
||||
|
||||
val internalIdPrefix = when (createTimelineParams) {
|
||||
|
||||
+13
-7
@@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
||||
import io.element.android.libraries.matrix.impl.room.join.map
|
||||
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
|
||||
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
||||
@@ -41,6 +42,7 @@ import org.matrix.rustcomponents.sdk.TimelineConfiguration
|
||||
import org.matrix.rustcomponents.sdk.TimelineFilter
|
||||
import org.matrix.rustcomponents.sdk.TimelineFocus
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk_base.EncryptionState
|
||||
import uniffi.matrix_sdk_ui.TimelineReadReceiptTracking
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
||||
@@ -66,12 +68,6 @@ class RustRoomFactory(
|
||||
private val mutex = Mutex()
|
||||
private val isDestroyed: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
private val eventFilters = TimelineConfig.excludedEvents
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { listStateEventType ->
|
||||
timelineEventFilterFactory.create(listStateEventType)
|
||||
}
|
||||
|
||||
suspend fun destroy() {
|
||||
withContext(NonCancellable + dispatcher) {
|
||||
mutex.withLock {
|
||||
@@ -128,10 +124,20 @@ class RustRoomFactory(
|
||||
operation = "sdkRoom.timelineWithConfiguration",
|
||||
description = "Get timeline from the SDK",
|
||||
) {
|
||||
val isEncrypted = when (roomInfo.encryptionState) {
|
||||
EncryptionState.ENCRYPTED -> true
|
||||
EncryptionState.NOT_ENCRYPTED -> false
|
||||
EncryptionState.UNKNOWN -> null
|
||||
}
|
||||
val timelineFilter = timelineEventFilterFactory.create(
|
||||
joinRule = roomInfo.joinRule?.map(),
|
||||
isEncrypted = isEncrypted,
|
||||
excludedStateTypes = TimelineConfig.excludedEvents,
|
||||
)
|
||||
sdkRoom.timelineWithConfiguration(
|
||||
TimelineConfiguration(
|
||||
focus = TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents),
|
||||
filter = eventFilters?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All,
|
||||
filter = timelineFilter?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All,
|
||||
internalIdPrefix = "live",
|
||||
dateDividerMode = DateDividerMode.DAILY,
|
||||
trackReadReceipts = TimelineReadReceiptTracking.ALL_EVENTS,
|
||||
|
||||
+32
-7
@@ -11,20 +11,45 @@ package io.element.android.libraries.matrix.impl.room
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import org.matrix.rustcomponents.sdk.FilterTimelineEventCondition
|
||||
import org.matrix.rustcomponents.sdk.FilterTimelineEventType
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventFilter
|
||||
import uniffi.matrix_sdk_ui.MembershipChangeFilter
|
||||
|
||||
interface TimelineEventFilterFactory {
|
||||
fun create(listStateEventType: List<StateEventType>): TimelineEventFilter
|
||||
fun create(
|
||||
joinRule: JoinRule?,
|
||||
isEncrypted: Boolean?,
|
||||
excludedStateTypes: List<StateEventType>
|
||||
): TimelineEventFilter?
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class RustTimelineEventFilterFactory : TimelineEventFilterFactory {
|
||||
override fun create(listStateEventType: List<StateEventType>): TimelineEventFilter {
|
||||
return TimelineEventFilter.excludeEventTypes(
|
||||
listStateEventType.map { stateEventType ->
|
||||
FilterTimelineEventType.State(stateEventType.map())
|
||||
}
|
||||
)
|
||||
override fun create(
|
||||
joinRule: JoinRule?,
|
||||
isEncrypted: Boolean?,
|
||||
excludedStateTypes: List<StateEventType>
|
||||
): TimelineEventFilter? {
|
||||
val excludedEventTypes = excludedStateTypes.map {
|
||||
FilterTimelineEventCondition.EventType(FilterTimelineEventType.State(it.map()))
|
||||
}
|
||||
// If the room is publicly joinable and not encrypted, we also want to exclude membership changes and profile changes,
|
||||
// as they will pollute the timelines since they're quite common and not add much value.
|
||||
val excludedMembershipChanges = if (joinRule !is JoinRule.Invite && isEncrypted == false) {
|
||||
listOf(
|
||||
FilterTimelineEventCondition.MembershipChange(MembershipChangeFilter.JOIN),
|
||||
FilterTimelineEventCondition.MembershipChange(MembershipChangeFilter.LEAVE),
|
||||
FilterTimelineEventCondition.ProfileChange,
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
return if (excludedEventTypes.isNotEmpty() || excludedMembershipChanges.isNotEmpty()) {
|
||||
TimelineEventFilter.exclude(excludedEventTypes + excludedMembershipChanges)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl.storage
|
||||
import io.element.android.libraries.androidutils.crypto.ClientSecret
|
||||
import io.element.android.libraries.core.data.ByteUnit
|
||||
import io.element.android.libraries.core.data.megaBytes
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
import org.matrix.rustcomponents.sdk.SqliteStoreBuilder as SdkSqliteStoreBuilder
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.storage
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
|
||||
interface SqliteStoreBuilderProvider {
|
||||
fun provide(sessionPaths: SessionPaths): SqliteStoreBuilder
|
||||
|
||||
-16
@@ -38,8 +38,6 @@ import io.element.android.libraries.matrix.impl.room.location.into
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.FilterEmptyDayPostProcessor
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.FilterPublicMembershipChangesPostProcessor
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LastForwardIndicatorsPostProcessor
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor
|
||||
@@ -125,8 +123,6 @@ class RustTimeline(
|
||||
private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock)
|
||||
private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode)
|
||||
private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode)
|
||||
private val publicMembershipChangesPostProcessor = FilterPublicMembershipChangesPostProcessor()
|
||||
private val emptyDayPostProcessor = FilterEmptyDayPostProcessor()
|
||||
|
||||
private data class RoomTimelineInfo(
|
||||
val roomCreators: ImmutableList<UserId>,
|
||||
@@ -252,18 +248,6 @@ class RustTimeline(
|
||||
hasMoreToLoadBackwards = backwardPaginationStatus.hasMoreToLoad,
|
||||
)
|
||||
}
|
||||
// This should be the first post processor after room beginning.
|
||||
.let { items ->
|
||||
publicMembershipChangesPostProcessor.process(
|
||||
items = items,
|
||||
joinRule = joinRule,
|
||||
isEncrypted = isEncrypted,
|
||||
)
|
||||
}
|
||||
// After removing public membership changes, we might end up with empty days, so we need to filter them out.
|
||||
.let { items ->
|
||||
emptyDayPostProcessor.process(items)
|
||||
}
|
||||
.let { items ->
|
||||
loadingIndicatorsPostProcessor.process(
|
||||
items = items,
|
||||
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
|
||||
/**
|
||||
* Post-processor to filter out day separators for days that don't contain any events.
|
||||
*/
|
||||
class FilterEmptyDayPostProcessor {
|
||||
/**
|
||||
* Filters out day separators from [items] for days that don't contain any events.
|
||||
*/
|
||||
fun process(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> = buildList {
|
||||
// The timeline is ordered by ascending timestamp, so events that happened during a day appear after the day separator for that day.
|
||||
// We can use this to determine if a day separator should be kept or not by traversing the list in reverse.
|
||||
var hasEvent = false
|
||||
for (item in items.asReversed()) {
|
||||
if (item is MatrixTimelineItem.Event) {
|
||||
hasEvent = true
|
||||
add(item)
|
||||
} else if (item is MatrixTimelineItem.Virtual && item.virtual is VirtualTimelineItem.DayDivider) {
|
||||
if (hasEvent) {
|
||||
add(item)
|
||||
hasEvent = false
|
||||
}
|
||||
} else {
|
||||
add(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Then reverse the result to restore the original order, minus the empty day separators.
|
||||
.asReversed()
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
|
||||
/**
|
||||
* Post-processor to filter out public membership changes for non-encrypted, publicly joinable rooms.
|
||||
*/
|
||||
class FilterPublicMembershipChangesPostProcessor {
|
||||
/**
|
||||
* Filters out public membership changes from [items] if the room is publicly joinable and not encrypted.
|
||||
*/
|
||||
fun process(
|
||||
items: List<MatrixTimelineItem>,
|
||||
joinRule: JoinRule?,
|
||||
isEncrypted: Boolean?,
|
||||
): List<MatrixTimelineItem> {
|
||||
return if (joinRule !is JoinRule.Invite && isEncrypted == false) {
|
||||
filterMembershipEvents(items)
|
||||
} else {
|
||||
items
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterMembershipEvents(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> = items.filter { item ->
|
||||
val eventContent = (item as? MatrixTimelineItem.Event)?.event?.content ?: return@filter true
|
||||
when (eventContent) {
|
||||
is RoomMembershipContent -> eventContent.change != null && eventContent.change !in listOf(MembershipChange.JOINED, MembershipChange.LEFT)
|
||||
is ProfileChangeContent -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.util
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
|
||||
class SessionPathsProvider(
|
||||
private val sessionStore: SessionStore,
|
||||
) {
|
||||
suspend fun provides(sessionId: SessionId): SessionPaths? {
|
||||
val sessionData = sessionStore.getSession(sessionId.value) ?: return null
|
||||
return sessionData.getSessionPaths()
|
||||
}
|
||||
}
|
||||
+2
@@ -13,6 +13,7 @@ package io.element.android.libraries.matrix.impl
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.data.bytes
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService
|
||||
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
|
||||
@@ -141,6 +142,7 @@ class RustMatrixClientTest {
|
||||
),
|
||||
) = RustMatrixClient(
|
||||
innerClient = client,
|
||||
sessionPaths = SessionPaths(fileDirectory = File("files"), cacheDirectory = File("cache")),
|
||||
sessionStore = sessionStore,
|
||||
appCoroutineScope = backgroundScope,
|
||||
sessionDelegate = aRustClientSessionDelegate(
|
||||
|
||||
+1
-1
@@ -10,8 +10,8 @@ package io.element.android.libraries.matrix.impl.mapper
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
|
||||
|
||||
+2
-1
@@ -9,11 +9,12 @@
|
||||
package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEventFilter
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventFilter
|
||||
|
||||
class FakeTimelineEventFilterFactory : TimelineEventFilterFactory {
|
||||
override fun create(listStateEventType: List<StateEventType>): TimelineEventFilter {
|
||||
override fun create(joinRule: JoinRule?, isEncrypted: Boolean?, excludedStateTypes: List<StateEventType>): TimelineEventFilter {
|
||||
return FakeFfiTimelineEventFilter()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.storage
|
||||
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
|
||||
class FakeSqliteStoreBuilderProvider : SqliteStoreBuilderProvider {
|
||||
override fun provide(sessionPaths: SessionPaths): SqliteStoreBuilder {
|
||||
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import org.junit.Test
|
||||
|
||||
private const val TODAY = 1_779_779_967_000
|
||||
private const val YESTERDAY = TODAY - 24 * 60 * 60 * 1000
|
||||
private const val DAY_BEFORE_YESTERDAY = YESTERDAY - 24 * 60 * 60 * 1000
|
||||
|
||||
class FilterEmptyDayPostProcessorTest {
|
||||
private val anEvent = MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("event"),
|
||||
event = anEventTimelineItem(),
|
||||
)
|
||||
|
||||
private fun aDaySeparator(timestmap: Long) = MatrixTimelineItem.Virtual(
|
||||
uniqueId = UniqueId("day_$timestmap"),
|
||||
virtual = VirtualTimelineItem.DayDivider(timestmap)
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators keeps day separator with events after it`() {
|
||||
val items = listOf(
|
||||
aDaySeparator(TODAY),
|
||||
anEvent,
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result[0]).isEqualTo(aDaySeparator(TODAY))
|
||||
assertThat(result[1]).isEqualTo(anEvent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators removes day separator with no events after it`() {
|
||||
val items = listOf(
|
||||
aDaySeparator(YESTERDAY),
|
||||
aDaySeparator(TODAY),
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators removes second day separator and keeps first when only first has events`() {
|
||||
val items = listOf(
|
||||
aDaySeparator(YESTERDAY),
|
||||
anEvent,
|
||||
aDaySeparator(TODAY),
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result[0]).isEqualTo(aDaySeparator(YESTERDAY))
|
||||
assertThat(result[1]).isEqualTo(anEvent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators handles multiple day separators in a row with no events`() {
|
||||
val items = listOf(
|
||||
aDaySeparator(TODAY),
|
||||
aDaySeparator(YESTERDAY),
|
||||
aDaySeparator(DAY_BEFORE_YESTERDAY),
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators keeps all items when no day separators`() {
|
||||
val items = listOf(
|
||||
anEvent.copy(uniqueId = UniqueId("event2")),
|
||||
anEvent,
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).hasSize(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators removes day separator preceded by non-event virtual item`() {
|
||||
val readMarker = MatrixTimelineItem.Virtual(
|
||||
uniqueId = UniqueId("readMarker"),
|
||||
virtual = VirtualTimelineItem.ReadMarker
|
||||
)
|
||||
val items = listOf(
|
||||
aDaySeparator(TODAY),
|
||||
readMarker,
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).hasSize(1)
|
||||
assertThat(result[0]).isEqualTo(readMarker)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators keeps day separator when non-event virtual items are between separator and event`() {
|
||||
val readMarker = MatrixTimelineItem.Virtual(
|
||||
uniqueId = UniqueId("readMarker"),
|
||||
virtual = VirtualTimelineItem.ReadMarker
|
||||
)
|
||||
val items = listOf(
|
||||
aDaySeparator(TODAY),
|
||||
readMarker,
|
||||
anEvent,
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).hasSize(3)
|
||||
assertThat(result[0]).isEqualTo(aDaySeparator(TODAY))
|
||||
}
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import org.junit.Test
|
||||
|
||||
class FilterPublicMembershipChangesPostProcessorTest {
|
||||
@Test
|
||||
fun `processor removes join, leave, and profile events in unencrypted public rooms`() {
|
||||
val timelineItems = listOf(
|
||||
roomCreateEvent,
|
||||
roomCreatorJoinEvent,
|
||||
otherMemberJoinEvent,
|
||||
messageEvent,
|
||||
otherMemberLeaveEvent,
|
||||
profileChangeEvent,
|
||||
)
|
||||
val expected = listOf(
|
||||
roomCreateEvent,
|
||||
messageEvent,
|
||||
)
|
||||
val processor = FilterPublicMembershipChangesPostProcessor()
|
||||
val processedItems = processor.process(
|
||||
timelineItems,
|
||||
joinRule = JoinRule.Public,
|
||||
isEncrypted = false,
|
||||
)
|
||||
assertThat(processedItems).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processor keeps all events in encrypted public rooms`() {
|
||||
val timelineItems = listOf(
|
||||
roomCreateEvent,
|
||||
roomCreatorJoinEvent,
|
||||
otherMemberJoinEvent,
|
||||
messageEvent,
|
||||
otherMemberLeaveEvent,
|
||||
profileChangeEvent,
|
||||
)
|
||||
val processor = FilterPublicMembershipChangesPostProcessor()
|
||||
val processedItems = processor.process(
|
||||
timelineItems,
|
||||
joinRule = JoinRule.Public,
|
||||
isEncrypted = true,
|
||||
)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processor keeps membership events in invite-only rooms`() {
|
||||
val timelineItems = listOf(
|
||||
roomCreateEvent,
|
||||
roomCreatorJoinEvent,
|
||||
otherMemberJoinEvent,
|
||||
messageEvent,
|
||||
otherMemberLeaveEvent,
|
||||
profileChangeEvent,
|
||||
)
|
||||
val processor = FilterPublicMembershipChangesPostProcessor()
|
||||
val processedItems = processor.process(
|
||||
timelineItems,
|
||||
joinRule = JoinRule.Invite,
|
||||
isEncrypted = null,
|
||||
)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.util
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class SessionPathsProviderTest {
|
||||
@Test
|
||||
fun `if session is not found, provides returns null`() = runTest {
|
||||
val sut = SessionPathsProvider(InMemorySessionStore())
|
||||
val result = sut.provides(A_SESSION_ID)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if session is found, provides returns the data`() = runTest {
|
||||
val store = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionPath = "/a/path/to/a/session",
|
||||
cachePath = "/a/path/to/a/cache",
|
||||
)
|
||||
)
|
||||
)
|
||||
val sut = SessionPathsProvider(store)
|
||||
val result = sut.provides(A_SESSION_ID)!!
|
||||
assertThat(result.fileDirectory.absolutePath).isEqualTo("/a/path/to/a/session")
|
||||
assertThat(result.cacheDirectory.absolutePath).isEqualTo("/a/path/to/a/cache")
|
||||
}
|
||||
}
|
||||
+3
@@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.MediaPreviewService
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||
import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
@@ -66,10 +67,12 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import java.io.File
|
||||
import java.util.Optional
|
||||
|
||||
class FakeMatrixClient(
|
||||
override val sessionId: SessionId = A_SESSION_ID,
|
||||
override val sessionPaths: SessionPaths = SessionPaths(fileDirectory = File("files"), cacheDirectory = File("cache")),
|
||||
override val deviceId: DeviceId = A_DEVICE_ID,
|
||||
override val sessionCoroutineScope: CoroutineScope = TestScope(),
|
||||
private val userDisplayName: String? = A_USER_NAME,
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
<string name="screen_bottom_sheet_create_dm_title">"Отправить приглашение?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_content">"У Вас нет других чатов с этим пользователем. Подтвердите, что это действительно кого Вы хотите пригласить, прежде чем продолжить."</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_title">"Начать чат с этим новым контактом?"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) пригласил(а) вас"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) пригласил(а) Вас"</string>
|
||||
</resources>
|
||||
|
||||
+11
-4
@@ -100,7 +100,11 @@ class AndroidLocalMediaActions(
|
||||
override suspend fun share(localMedia: LocalMedia): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE)
|
||||
runCatchingExceptions {
|
||||
val shareableUri = localMedia.toShareableUri()
|
||||
// Make a copy of the shared file in the cache directory, otherwise the original file will be gone once this screen is dismissed
|
||||
// and will prevent sharing the media to another room inside the app.
|
||||
val copiedFile = localMedia.uri.toFile()
|
||||
.copyTo(File(context.cacheDir, "temp/media/" + (localMedia.uri.lastPathSegment ?: "shared_file")), true)
|
||||
val shareableUri = copiedFile.toShareableUri()
|
||||
val shareMediaIntent = Intent(Intent.ACTION_SEND)
|
||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.putExtra(Intent.EXTRA_STREAM, shareableUri)
|
||||
@@ -157,10 +161,13 @@ class AndroidLocalMediaActions(
|
||||
}
|
||||
}
|
||||
|
||||
private fun LocalMedia.toShareableUri(): Uri {
|
||||
val mediaAsFile = this.toFile()
|
||||
private fun File.toShareableUri(): Uri {
|
||||
val authority = "${buildMeta.applicationId}.fileprovider"
|
||||
return FileProvider.getUriForFile(context, authority, mediaAsFile).normalizeScheme()
|
||||
return FileProvider.getUriForFile(context, authority, this).normalizeScheme()
|
||||
}
|
||||
|
||||
private fun LocalMedia.toShareableUri(): Uri {
|
||||
return this.toFile().toShareableUri()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_media_details_title">"Інфармацыя пра файл"</string>
|
||||
</resources>
|
||||
@@ -15,8 +15,8 @@
|
||||
<item quantity="few">"%d уведомления"</item>
|
||||
<item quantity="many">"%d уведомлений"</item>
|
||||
</plurals>
|
||||
<string name="notification_error_unified_push_unregistered_android">"Не удалось зарегистрировать дистрибьютора уведомлений UnifiedPush, поэтому вы больше не будете получать уведомления. Проверьте настройки уведомлений в приложении и статус дистрибьютора push-уведомлений."</string>
|
||||
<string name="notification_fallback_content">"У вас есть новые сообщения."</string>
|
||||
<string name="notification_error_unified_push_unregistered_android">"Не удалось зарегистрировать дистрибьютора уведомлений UnifiedPush, поэтому Вы больше не будете получать уведомления. Проверьте настройки уведомлений в приложении и статус дистрибьютора push-уведомлений."</string>
|
||||
<string name="notification_fallback_content">"У Вас есть новые сообщения."</string>
|
||||
<plurals name="notification_fallback_n_content">
|
||||
<item quantity="one">"У Вас %d новое сообщение."</item>
|
||||
<item quantity="few">"У Вас %d новых сообщения."</item>
|
||||
@@ -32,8 +32,8 @@
|
||||
<item quantity="few">"%d приглашения"</item>
|
||||
<item quantity="many">"%d приглашений"</item>
|
||||
</plurals>
|
||||
<string name="notification_invite_body">"Пригласил(а) вас в чат"</string>
|
||||
<string name="notification_invite_body_with_sender">"%1$s пригласил(а) вас в чат"</string>
|
||||
<string name="notification_invite_body">"Пригласил(а) Вас в чат"</string>
|
||||
<string name="notification_invite_body_with_sender">"%1$s пригласил(а) Вас в чат"</string>
|
||||
<string name="notification_mentioned_you_body">"Упомянул(а) вас: %1$s"</string>
|
||||
<string name="notification_new_messages">"Новые сообщения"</string>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
@@ -44,12 +44,12 @@
|
||||
<string name="notification_reaction_body">"Отреагировал(а) на %1$s"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Пометить как прочитанное"</string>
|
||||
<string name="notification_room_action_quick_reply">"Быстрый ответ"</string>
|
||||
<string name="notification_room_invite_body">"Пригласил(а) вас в комнату"</string>
|
||||
<string name="notification_room_invite_body_with_sender">"%1$s пригласил(а) вас в комнату"</string>
|
||||
<string name="notification_room_invite_body">"Пригласил(а) Вас в комнату"</string>
|
||||
<string name="notification_room_invite_body_with_sender">"%1$s пригласил(а) Вас в комнату"</string>
|
||||
<string name="notification_sender_me">"Я"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s упомянул(а) или ответил(а)"</string>
|
||||
<string name="notification_space_invite_body">"Пригласил(а) вас в пространство"</string>
|
||||
<string name="notification_space_invite_body_with_sender">"%1$s пригласил(а) вас в пространство"</string>
|
||||
<string name="notification_space_invite_body">"Пригласил(а) Вас в пространство"</string>
|
||||
<string name="notification_space_invite_body_with_sender">"%1$s пригласил(а) Вас в пространство"</string>
|
||||
<string name="notification_test_push_notification_content">"Вы просматриваете уведомление! Нажмите на меня!"</string>
|
||||
<string name="notification_thread_in_room">"Обсуждение в %1$s"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<string name="rich_text_editor_bullet_list">"Пераключыць маркіраваны спіс"</string>
|
||||
<string name="rich_text_editor_close_formatting_options">"Закрыць параметры фарматавання"</string>
|
||||
<string name="rich_text_editor_code_block">"Пераключыць блок кода"</string>
|
||||
<string name="rich_text_editor_composer_encrypted_placeholder">"Зашыфраванае паведамленне…"</string>
|
||||
<string name="rich_text_editor_composer_placeholder">"Паведамленне…"</string>
|
||||
<string name="rich_text_editor_composer_unencrypted_placeholder">"Незашыфраванае паведамленне…"</string>
|
||||
<string name="rich_text_editor_create_link">"Стварыць спасылку"</string>
|
||||
<string name="rich_text_editor_edit_link">"Рэдагаваць спасылку"</string>
|
||||
<string name="rich_text_editor_format_bold">"Ужыць тоўсты шрыфт"</string>
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
<string name="troubleshoot_notifications_screen_notice">"Выполните тесты, чтобы выявить проблемы в конфигурации, из-за которой уведомления могут работать не так, как ожидалось."</string>
|
||||
<string name="troubleshoot_notifications_screen_quick_fix_action">"Попытаться исправить"</string>
|
||||
<string name="troubleshoot_notifications_screen_success">"Все тесты прошли успешно."</string>
|
||||
<string name="troubleshoot_notifications_screen_title">"Уведомления об устранении неполадок"</string>
|
||||
<string name="troubleshoot_notifications_screen_title">"Устранение неполадок с уведомлениями"</string>
|
||||
<string name="troubleshoot_notifications_screen_waiting">"Некоторые тесты требуют вашего внимания. Пожалуйста, проверьте подробности."</string>
|
||||
</resources>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<string name="a11y_edit_room_address_hint">"Поўны адрас будзе %1$s"</string>
|
||||
<string name="a11y_encryption_details">"Дэталі шыфравання"</string>
|
||||
<string name="a11y_hide_password">"Схаваць пароль"</string>
|
||||
<string name="a11y_info">"Інфармацыя"</string>
|
||||
<string name="a11y_join_call">"Далучыцца да выкліку"</string>
|
||||
<string name="a11y_jump_to_bottom">"Перайсці ўніз"</string>
|
||||
<string name="a11y_notifications_mentions_only">"Толькі згадкі"</string>
|
||||
@@ -72,12 +73,16 @@
|
||||
<string name="action_deactivate_account">"Дэактываваць уліковы запіс"</string>
|
||||
<string name="action_decline">"Адхіліць"</string>
|
||||
<string name="action_decline_and_block">"Адхіліць і заблакіраваць"</string>
|
||||
<string name="action_delete">"Выдаліць"</string>
|
||||
<string name="action_delete_account">"Выдаліць уліковы запіс"</string>
|
||||
<string name="action_delete_file">"Выдаліць файл"</string>
|
||||
<string name="action_delete_poll">"Выдаліць апытанне"</string>
|
||||
<string name="action_deselect_all">"Зняць выбар з усіх"</string>
|
||||
<string name="action_disable">"Адключыць"</string>
|
||||
<string name="action_discard">"Адмяніць"</string>
|
||||
<string name="action_dismiss">"Aдхіліць"</string>
|
||||
<string name="action_done">"Гатова"</string>
|
||||
<string name="action_download">"Спампаваць"</string>
|
||||
<string name="action_edit">"Рэдагаваць"</string>
|
||||
<string name="action_edit_caption">"Рэдагаваць подпіс"</string>
|
||||
<string name="action_edit_poll">"Рэдагаваць апытанне"</string>
|
||||
@@ -119,6 +124,7 @@
|
||||
<string name="action_react">"Рэакцыя"</string>
|
||||
<string name="action_reject">"Адхіліць"</string>
|
||||
<string name="action_remove">"Выдаліць"</string>
|
||||
<string name="action_remove_message">"Выдаліць паведамленне"</string>
|
||||
<string name="action_reply">"Адказаць"</string>
|
||||
<string name="action_reply_in_thread">"Адказаць у гутаркі"</string>
|
||||
<string name="action_report">"Паскардзіцца"</string>
|
||||
@@ -133,7 +139,7 @@
|
||||
<string name="action_select_all">"Выбраць усё"</string>
|
||||
<string name="action_send">"Адправіць"</string>
|
||||
<string name="action_send_message">"Адправіць паведамленне"</string>
|
||||
<string name="action_share">"Падзяліцца"</string>
|
||||
<string name="action_share">"Абагуліць"</string>
|
||||
<string name="action_share_link">"Абагуліць спасылку"</string>
|
||||
<string name="action_show">"Паказаць"</string>
|
||||
<string name="action_sign_in_again">"Увайдзіце яшчэ раз"</string>
|
||||
@@ -214,8 +220,10 @@
|
||||
<string name="common_message_removed">"Паведамленне выдалена"</string>
|
||||
<string name="common_modern">"Сучасны"</string>
|
||||
<string name="common_mute">"Адкл. гук"</string>
|
||||
<string name="common_name">"Імя"</string>
|
||||
<string name="common_no_results">"Вынікаў няма"</string>
|
||||
<string name="common_no_room_name">"Няма назвы пакоя"</string>
|
||||
<string name="common_not_encrypted">"Не зашыфравана"</string>
|
||||
<string name="common_offline">"Па-за сеткай"</string>
|
||||
<string name="common_open_source_licenses">"Ліцэнзіі з адкрытым зыходным кодам"</string>
|
||||
<string name="common_or">"або"</string>
|
||||
@@ -227,6 +235,7 @@
|
||||
<string name="common_please_wait">"Калі ласка, пачакайце…"</string>
|
||||
<string name="common_poll_end_confirmation">"Вы ўпэўнены, што хочаце скончыць гэтае апытанне?"</string>
|
||||
<string name="common_poll_summary">"Апытанне: %1$s"</string>
|
||||
<string name="common_poll_summary_prefix">"Апытанне"</string>
|
||||
<string name="common_poll_total_votes">"Усяго галасоў: %1$s"</string>
|
||||
<string name="common_poll_undisclosed_text">"Вынікі будуць паказаны пасля завяршэння апытання"</string>
|
||||
<plurals name="common_poll_votes_count">
|
||||
@@ -241,6 +250,7 @@
|
||||
<string name="common_reactions">"Рэакцыі"</string>
|
||||
<string name="common_recovery_key">"Ключ аднаўлення"</string>
|
||||
<string name="common_refreshing">"Абнаўленне…"</string>
|
||||
<string name="common_removing">"Выдаленне…"</string>
|
||||
<string name="common_replying_to">"Адказвае на %1$s"</string>
|
||||
<string name="common_report_a_problem">"Паведаміць аб праблеме"</string>
|
||||
<string name="common_report_submitted">"Скарга прынята"</string>
|
||||
@@ -262,6 +272,7 @@
|
||||
<string name="common_server_not_supported">"Сервер не падтрымліваецца"</string>
|
||||
<string name="common_server_url">"URL-адрас сервера"</string>
|
||||
<string name="common_settings">"Налады"</string>
|
||||
<string name="common_share_space">"Абагуліць прастору"</string>
|
||||
<string name="common_shared_location">"Абагулена месцазнаходжанне"</string>
|
||||
<string name="common_signing_out">"Выхад"</string>
|
||||
<string name="common_something_went_wrong">"Нешта пайшло не так"</string>
|
||||
@@ -315,6 +326,11 @@
|
||||
<string name="invite_friends_text">"Гэй, пагавары са мной у %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Паведаміць аб памылцы з дапамогай Rageshake"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Здымак экрана"</string>
|
||||
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
|
||||
<string name="screen_create_poll_options_section_title">"Параметры"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Выдаліць %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Налады"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Не ўдалося выбраць носьбіт, паўтарыце спробу."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Націсніце на паведамленне і абярыце «%1$s », каб уключыць сюды."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Замацуеце важныя паведамленні, каб іх можна было лёгка знайсці"</string>
|
||||
@@ -335,8 +351,8 @@
|
||||
<string name="screen_room_pinned_banner_loading_description">"Загрузка паведамлення…"</string>
|
||||
<string name="screen_room_pinned_banner_view_all_button_title">"Паглядзець усе"</string>
|
||||
<string name="screen_room_title">"Чат"</string>
|
||||
<string name="screen_share_location_title">"Падзяліцца месцазнаходжаннем"</string>
|
||||
<string name="screen_share_my_location_action">"Падзяліцца маім месцазнаходжаннем"</string>
|
||||
<string name="screen_share_location_title">"Абагуліць месцазнаходжанне"</string>
|
||||
<string name="screen_share_my_location_action">"Абагуліць мае месцазнаходжанне"</string>
|
||||
<string name="screen_share_open_apple_maps">"Адкрыць у Apple Maps"</string>
|
||||
<string name="screen_share_open_google_maps">"Адкрыць у Google Maps"</string>
|
||||
<string name="screen_share_open_osm_maps">"Адкрыць у OpenStreetMap"</string>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<string name="a11y_send_files">"Отправить файлы"</string>
|
||||
<string name="a11y_sender_location">"Местоположение отправителя"</string>
|
||||
<string name="a11y_sent_by_sender_at_date">"Отправлено пользователем %1$s в %2$s"</string>
|
||||
<string name="a11y_session_verification_time_limited_action_required">"Требуется действие, на которое есть ограничение по времени, у вас есть одна минута для проверки"</string>
|
||||
<string name="a11y_session_verification_time_limited_action_required">"Требуется действие, на которое есть ограничение по времени, у Вас есть одна минута для проверки"</string>
|
||||
<string name="a11y_settings_with_required_action">"Настройки, требуется действие"</string>
|
||||
<string name="a11y_show_password">"Показать пароль"</string>
|
||||
<string name="a11y_start_call">"Начать звонок"</string>
|
||||
@@ -101,6 +101,7 @@
|
||||
<string name="action_decline_and_block">"Отклонить и заблокировать"</string>
|
||||
<string name="action_delete">"Удалить"</string>
|
||||
<string name="action_delete_account">"Удалить аккаунт"</string>
|
||||
<string name="action_delete_file">"Удалить файл"</string>
|
||||
<string name="action_delete_poll">"Удалить опрос"</string>
|
||||
<string name="action_deselect_all">"Отменить выбор"</string>
|
||||
<string name="action_disable">"Отключить"</string>
|
||||
@@ -133,7 +134,7 @@
|
||||
<string name="action_leave_conversation">"Покинуть беседу"</string>
|
||||
<string name="action_leave_room">"Покинуть комнату"</string>
|
||||
<string name="action_leave_space">"Покинуть пространство"</string>
|
||||
<string name="action_load_more">"Загрузить еще"</string>
|
||||
<string name="action_load_more">"Загрузить ещё"</string>
|
||||
<string name="action_manage_account">"Настройки аккаунта"</string>
|
||||
<string name="action_manage_account_and_devices">"Управление учетной записью и устройствами"</string>
|
||||
<string name="action_manage_devices">"Управление устройствами"</string>
|
||||
@@ -179,7 +180,7 @@
|
||||
<string name="action_show">"Показать"</string>
|
||||
<string name="action_sign_in_again">"Повторить вход"</string>
|
||||
<string name="action_signout">"Удалить это устройство"</string>
|
||||
<string name="action_signout_anyway">"Все равно удалить это устройство"</string>
|
||||
<string name="action_signout_anyway">"Всё равно удалить это устройство"</string>
|
||||
<string name="action_skip">"Пропустить"</string>
|
||||
<string name="action_start">"Начать"</string>
|
||||
<string name="action_start_chat">"Начать чат"</string>
|
||||
@@ -196,8 +197,8 @@
|
||||
<string name="action_view_in_timeline">"Просмотреть в хронологии"</string>
|
||||
<string name="action_view_source">"Показать источник"</string>
|
||||
<string name="action_yes">"Да"</string>
|
||||
<string name="action_yes_try_again">"Да, попробовать еще раз"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_description">"Теперь ваш сервер поддерживает новый, более быстрый протокол. Чтобы обновить его прямо сейчас, выйдите и войдите в свою учётную запись снова. Сделав это сейчас, вы сможете избежать принудительного выхода из системы при последующем удалении старого протокола."</string>
|
||||
<string name="action_yes_try_again">"Да, попробовать ещё раз"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_description">"Теперь Ваш сервер поддерживает новый, более быстрый протокол. Чтобы обновить его прямо сейчас, выйдите и войдите в свою учётную запись снова. Сделав это сейчас, Вы сможете избежать принудительного выхода из системы при последующем удалении старого протокола."</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_title">"Доступно обновление"</string>
|
||||
<string name="common_about">"О приложении"</string>
|
||||
<string name="common_acceptable_use_policy">"Политика допустимого использования"</string>
|
||||
@@ -377,7 +378,7 @@
|
||||
<string name="common_shared_space">"Общее пространство"</string>
|
||||
<string name="common_signing_out">"Удаление устройства"</string>
|
||||
<string name="common_something_went_wrong">"Что-то пошло не так"</string>
|
||||
<string name="common_something_went_wrong_message">"Мы столкнулись с проблемой. Пожалуйста, попробуйте еще раз."</string>
|
||||
<string name="common_something_went_wrong_message">"Мы столкнулись с проблемой. Пожалуйста, попробуйте ещё раз."</string>
|
||||
<string name="common_space">"Пространство"</string>
|
||||
<string name="common_space_members">"Участники пространства"</string>
|
||||
<string name="common_space_topic_placeholder">"О чём это пространство?"</string>
|
||||
@@ -429,8 +430,8 @@
|
||||
<string name="common_waiting_live_location">"Ожидание трансляции местоположения…"</string>
|
||||
<string name="common_world_readable_history">"Любой может видеть историю"</string>
|
||||
<string name="common_you">"Вы"</string>
|
||||
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) поделился этим сообщением, поскольку вас не было в комнате, когда оно было отправлено."</string>
|
||||
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"%1$s поделился этим сообщением, поскольку вас не было в комнате, когда оно было отправлено."</string>
|
||||
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) поделился(ась) этим сообщением, поскольку Вас не было в комнате, когда оно было отправлено."</string>
|
||||
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"%1$s поделился(ась) этим сообщением, поскольку Вас не было в комнате, когда оно было отправлено."</string>
|
||||
<string name="crypto_history_visible">"Эта комната настроена так, что новые участники могут видеть историю. %1$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation">"Личность %1$s была сброшена. %2$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new">"Личность %1$s %2$s была сброшена. %3$s"</string>
|
||||
@@ -453,11 +454,11 @@
|
||||
<string name="dialog_title_error">"Ошибка"</string>
|
||||
<string name="dialog_title_success">"Успешно"</string>
|
||||
<string name="dialog_title_warning">"Предупреждение"</string>
|
||||
<string name="dialog_unsaved_changes_description">"У вас есть несохраненные изменения."</string>
|
||||
<string name="dialog_unsaved_changes_description">"У Вас есть несохраненные изменения."</string>
|
||||
<string name="dialog_unsaved_changes_description_android">"Изменения не сохранены. Вы действительно хотите вернуться?"</string>
|
||||
<string name="dialog_unsaved_changes_title">"Сохранить изменения?"</string>
|
||||
<string name="dialog_video_quality_selector_subtitle_file_size">"Максимально допустимый размер файла: %1$s"</string>
|
||||
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Выберите качество для видео, которое вы хотите загрузить."</string>
|
||||
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Выберите качество видео, которое Вы хотите загрузить."</string>
|
||||
<string name="dialog_video_quality_selector_title">"Выберите качество загружаемого видео"</string>
|
||||
<string name="emoji_picker_search_placeholder">"Поиск эмодзи"</string>
|
||||
<string name="error_account_already_logged_in">"Вы уже вошли на данном устройстве как %1$s."</string>
|
||||
@@ -498,7 +499,7 @@
|
||||
<item quantity="many">"%1$d людей"</item>
|
||||
</plurals>
|
||||
<string name="screen_live_location_sheet_title">"На карте"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Не удалось выбрать медиа, попробуйте еще раз."</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Не удалось выбрать медиа, попробуйте ещё раз."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Нажмите на сообщение и выберите «%1$s», чтобы добавить его сюда."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Закрепите важные сообщения, чтобы их можно было легко найти"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
@@ -506,19 +507,19 @@
|
||||
<item quantity="few">"%1$d закреплённых сообщения"</item>
|
||||
<item quantity="many">"%1$d закреплённых сообщений"</item>
|
||||
</plurals>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Закрепленные сообщения"</string>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Закреплённые сообщения"</string>
|
||||
<string name="screen_reset_identity_confirmation_subtitle">"Вы собираетесь перейти в свой аккаунт %1$s, чтобы сбросить личность. После этого Вы вернётесь в приложение."</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"Не можете подтвердить? Перейдите в свой аккаунт, чтобы сбросить свою личность."</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_primary_button_title">"Сбросить верификацию и отправить"</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_subtitle">"Вы можете либо сбросить подтверждение и всё равно отправить это сообщение, либо отменить его сейчас и повторить попытку после повторного подтверждения %1$s."</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_title">"Ваше сообщение не было отправлено, потому что подтвержденная идентификации %1$s была сброшена"</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_primary_button_title">"Все равно отправить сообщение"</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_subtitle">"У %1$s есть одно или несколько неподтвержденных устройств. Вы все равно можете отправить сообщение или отменить его пока и повторить попытку позже, когда %2$s подтвердить все свои устройства."</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_primary_button_title">"Всё равно отправить сообщение"</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_subtitle">"У %1$s есть одно или несколько неподтвержденных устройств. Вы всё равно можете отправить сообщение или отменить его и повторить попытку позже, когда %2$s подтвердит все свои устройства."</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_title">"Ваше сообщение не было отправлено, потому что %1$s имеет неподтвержденные устройства"</string>
|
||||
<string name="screen_resolve_send_failure_you_unsigned_device_subtitle">"Одно или несколько ваших устройств не подтверждены. Вы можете отправить сообщение в любом случае или отменить его пока и повторить попытку позже, подтвердив все свои устройства."</string>
|
||||
<string name="screen_resolve_send_failure_you_unsigned_device_title">"Ваше сообщение не было отправлено, поскольку вы не подтвердили одно или несколько своих устройств."</string>
|
||||
<string name="screen_room_change_role_administrators_or_owners_title">"Редактировать владельцев и администраторов"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не удалось обработать медиафайл для загрузки, попробуйте ещё раз."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Не удалось получить данные о пользователе"</string>
|
||||
<string name="screen_room_event_pill">"Сообщение в %1$s"</string>
|
||||
<string name="screen_room_grouped_state_events_expand">"Развернуть"</string>
|
||||
@@ -526,9 +527,9 @@
|
||||
<string name="screen_room_live_location_banner">"Поделиться текущим местоположением…"</string>
|
||||
<string name="screen_room_permalink_same_room_android">"Эта комната уже просматривается!"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s из %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s закрепленные сообщения"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Закреплённые сообщения"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Загрузка сообщения…"</string>
|
||||
<string name="screen_room_pinned_banner_view_all_button_title">"Посмотреть все"</string>
|
||||
<string name="screen_room_pinned_banner_view_all_button_title">"Посмотреть Все"</string>
|
||||
<string name="screen_room_title">"Чат"</string>
|
||||
<string name="screen_share_location_title">"Поделиться местоположением"</string>
|
||||
<string name="screen_share_my_location_action">"Поделиться моим местоположением"</string>
|
||||
@@ -555,4 +556,6 @@
|
||||
<string name="timeline_decryption_failure_historical_event_user_not_joined">"Вы не имеете доступа к этому сообщению"</string>
|
||||
<string name="timeline_decryption_failure_unable_to_decrypt">"Не удалось расшифровать сообщение"</string>
|
||||
<string name="timeline_decryption_failure_withheld_unverified">"Это сообщение было заблокировано либо потому, что вы не подтвердили свое устройство, либо потому, что отправителю необходимо подтвердить вашу цифровую личность."</string>
|
||||
<string name="vulkan_not_supported_dialog_content_android">"Ваше устройство слишком старое, требуется устройство с Android 8 или более новой версией."</string>
|
||||
<string name="vulkan_not_supported_dialog_title_android">"Отображение карт не поддерживается"</string>
|
||||
</resources>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<item quantity="few">"Введено %1$d цифри"</item>
|
||||
<item quantity="many">"Введено %1$d цифр"</item>
|
||||
</plurals>
|
||||
<string name="a11y_duration">"Тривалість: %1$s"</string>
|
||||
<string name="a11y_edit_avatar">"Редагувати аватар"</string>
|
||||
<string name="a11y_edit_room_address_hint">"Повна адреса буде %1$s"</string>
|
||||
<string name="a11y_encryption_details">"Подробиці шифрування"</string>
|
||||
@@ -18,6 +19,7 @@
|
||||
<string name="a11y_info">"Інформація"</string>
|
||||
<string name="a11y_join_call">"Приєднатися до виклику"</string>
|
||||
<string name="a11y_jump_to_bottom">"Перейти вниз"</string>
|
||||
<string name="a11y_jump_to_unread">"Перейти до непрочитаних"</string>
|
||||
<string name="a11y_move_the_map_to_my_location">"Перемістити карту до мого місця перебування"</string>
|
||||
<string name="a11y_notifications_mentions_only">"Тільки згадки"</string>
|
||||
<string name="a11y_notifications_muted">"Звук вимкнено"</string>
|
||||
@@ -34,6 +36,7 @@
|
||||
<string name="a11y_playback_speed">"Швидкість програвання"</string>
|
||||
<string name="a11y_poll">"Опитування"</string>
|
||||
<string name="a11y_poll_end">"Опитування завершено"</string>
|
||||
<string name="a11y_position">"Позиція: %1$s"</string>
|
||||
<string name="a11y_qr_code">"QR-код"</string>
|
||||
<string name="a11y_react_with">"Реагувати з%1$s"</string>
|
||||
<string name="a11y_react_with_other_emojis">"Відреагувати іншими смайликами"</string>
|
||||
@@ -50,12 +53,15 @@
|
||||
<string name="a11y_room_avatar">"Аватар кімнати"</string>
|
||||
<string name="a11y_send_files">"Надіслати файли"</string>
|
||||
<string name="a11y_sender_location">"Локація відправника"</string>
|
||||
<string name="a11y_sent_by_sender_at_date">"Надіслано %1$s о %2$s"</string>
|
||||
<string name="a11y_session_verification_time_limited_action_required">"Необхідно виконати дію, обмежену в часі, у вас є одна хвилина для верифікації"</string>
|
||||
<string name="a11y_settings_with_required_action">"Налаштування, потрібні дії"</string>
|
||||
<string name="a11y_show_password">"Показати пароль"</string>
|
||||
<string name="a11y_start_call">"Розпочати виклик"</string>
|
||||
<string name="a11y_start_video_call">"Розпочати відеодзвінок"</string>
|
||||
<string name="a11y_start_voice_call">"Розпочати голосовий дзвінок"</string>
|
||||
<string name="a11y_thread_in_room">"Гілка в %1$s"</string>
|
||||
<string name="a11y_threads_in_room">"Гілки в %1$s"</string>
|
||||
<string name="a11y_tombstoned_room">"Кімната більше не використовується"</string>
|
||||
<string name="a11y_user_avatar">"Аватар користувача"</string>
|
||||
<string name="a11y_user_menu">"Меню користувача"</string>
|
||||
@@ -95,6 +101,7 @@
|
||||
<string name="action_decline_and_block">"Відхилити та заблокувати"</string>
|
||||
<string name="action_delete">"Видалити"</string>
|
||||
<string name="action_delete_account">"Видалити обліковий запис"</string>
|
||||
<string name="action_delete_file">"Видалити файл"</string>
|
||||
<string name="action_delete_poll">"Видалити опитування"</string>
|
||||
<string name="action_deselect_all">"Скасувати вибір усіх"</string>
|
||||
<string name="action_disable">"Вимкнути"</string>
|
||||
@@ -302,6 +309,7 @@
|
||||
<string name="common_please_wait">"Будь ласка, зачекайте…"</string>
|
||||
<string name="common_poll_end_confirmation">"Ви впевнені, що хочете закінчити це опитування?"</string>
|
||||
<string name="common_poll_summary">"Опитування: %1$s"</string>
|
||||
<string name="common_poll_summary_prefix">"Опитування"</string>
|
||||
<string name="common_poll_total_votes">"Всього голосів: %1$s"</string>
|
||||
<string name="common_poll_undisclosed_text">"Результати будуть показані після завершення опитування"</string>
|
||||
<plurals name="common_poll_votes_count">
|
||||
@@ -473,6 +481,9 @@
|
||||
<string name="error_unknown">"Вибачте, сталася помилка"</string>
|
||||
<string name="invite_friends_rich_title">"🔐️ Приєднуйтеся до мене в %1$s"</string>
|
||||
<string name="invite_friends_text">"Вітаю, поспілкуйтеся зі мною в %1$s: %2$s"</string>
|
||||
<string name="live_location_sharing_foreground_service_channel_title_android">"Обмін місцезнаходженням у реальному часі"</string>
|
||||
<string name="live_location_sharing_foreground_service_message_android">"Триває обмін місцезнаходженням"</string>
|
||||
<string name="live_location_sharing_foreground_service_title_android">"%1$s Місцезнаходження в реальному часі"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Повідомити про ваду за допомогою Rageshake"</string>
|
||||
<string name="screen_bug_report_a11y_screenshot">"Знімок екрана"</string>
|
||||
|
||||
Reference in New Issue
Block a user