Merge branch 'develop' into feature/fga/user_moderation_bottomsheet

This commit is contained in:
ganfra 2025-05-23 17:28:48 +02:00
commit 03e1926e14
250 changed files with 3339 additions and 2742 deletions

View file

@ -53,7 +53,7 @@ fun Activity.openUrlInChromeCustomTab(
// Disable download button
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON", true)
// Disable bookmark button
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_START_BUTTON", true)
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_STAR_BUTTON", true)
intent.putExtra(Browser.EXTRA_HEADERS, Bundle().apply {
putString("Accept-Language", Locale.getDefault().toLanguageTag())
})

View file

@ -20,7 +20,7 @@
<string name="state_event_room_ban">"%1$s hat %2$s gesperrt"</string>
<string name="state_event_room_ban_by_you">"Du hast %1$s gesperrt"</string>
<string name="state_event_room_ban_by_you_with_reason">"Sie haben gesperrt %1$s: %2$s"</string>
<string name="state_event_room_ban_with_reason">"%1$sverboten%2$s: %3$s"</string>
<string name="state_event_room_ban_with_reason">"%1$s verboten %2$s: %3$s"</string>
<string name="state_event_room_created">"%1$s hat den Raum erstellt"</string>
<string name="state_event_room_created_by_you">"Du hast den Raum erstellt"</string>
<string name="state_event_room_invite">"%1$s hat %2$s eingeladen"</string>
@ -45,7 +45,7 @@
<string name="state_event_room_name_changed_by_you">"Du hast den Raum-Namen geändert in: %1$s"</string>
<string name="state_event_room_name_removed">"%1$s hat den Raum-Namen entfernt"</string>
<string name="state_event_room_name_removed_by_you">"Du hast den Raum-Namen entfernt"</string>
<string name="state_event_room_none">"%1$shat keine Änderungen vorgenommen"</string>
<string name="state_event_room_none">"%1$s hat keine Änderungen vorgenommen"</string>
<string name="state_event_room_none_by_you">"Du hast keine Änderungen vorgenommen"</string>
<string name="state_event_room_pinned_events_changed">"%1$s hat die fixierten Nachrichten geändert"</string>
<string name="state_event_room_pinned_events_changed_by_you">"Du hast die fixierten Nachrichten geändert"</string>
@ -58,7 +58,7 @@
<string name="state_event_room_remove">"%1$s hat %2$s entfernt"</string>
<string name="state_event_room_remove_by_you">"Du hast %1$s entfernt"</string>
<string name="state_event_room_remove_by_you_with_reason">"Sie haben %1$s entfernt: %2$s"</string>
<string name="state_event_room_remove_with_reason">"%1$sentfernt%2$s: %3$s"</string>
<string name="state_event_room_remove_with_reason">"%1$s entfernt %2$s: %3$s"</string>
<string name="state_event_room_third_party_invite">"%1$s hat eine Einladung an %2$s gesendet, dem Raum beizutreten"</string>
<string name="state_event_room_third_party_invite_by_you">"Du hast eine Einladung an %1$s gesendet, dem Raum beizutreten"</string>
<string name="state_event_room_third_party_revoked_invite">"%1$s hat die Einladung an %2$s zum Betreten des Raums zurückgezogen"</string>

View file

@ -147,13 +147,6 @@ enum class FeatureFlags(
defaultValue = { true },
isFinished = false,
),
EventCache(
key = "feature.event_cache",
title = "Use SDK Event cache",
description = "Warning: you must kill and restart the app for the change to take effect.",
defaultValue = { true },
isFinished = false,
),
PrintLogsToLogcat(
key = "feature.print_logs_to_logcat",
title = "Print logs to logcat",

View file

@ -162,6 +162,11 @@ interface MatrixClient {
fun canDeactivateAccount(): Boolean
suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit>
/**
* Check if the user can report a room.
*/
suspend fun canReportRoom(): Boolean
}
/**

View file

@ -43,7 +43,10 @@ interface MatrixAuthenticationService {
/**
* Get the Oidc url to display to the user.
*/
suspend fun getOidcUrl(prompt: OidcPrompt): Result<OidcDetails>
suspend fun getOidcUrl(
prompt: OidcPrompt,
loginHint: String?,
): Result<OidcDetails>
/**
* Cancel Oidc login sequence.

View file

@ -7,4 +7,6 @@
package io.element.android.libraries.matrix.api.auth.qrlogin
interface MatrixQrCodeLoginData
interface MatrixQrCodeLoginData {
fun serverName(): String?
}

View file

@ -55,6 +55,12 @@ interface BaseRoom : Closeable {
*/
fun info(): RoomInfo = roomInfoFlow.value
/**
* A one-to-one is a room with exactly 2 members.
* See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules).
*/
val isOneToOne: Boolean get() = info().activeMembersCount == 2L
/**
* Try to load the room members and update the membersFlow.
*/

View file

@ -9,34 +9,21 @@ package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
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.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.io.File
interface JoinedRoom : BaseRoom {
val syncUpdateFlow: StateFlow<Long>
@ -50,12 +37,6 @@ interface JoinedRoom : BaseRoom {
*/
val knockRequestsFlow: Flow<List<KnockRequest>>
/**
* A one-to-one is a room with exactly 2 members.
* See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules).
*/
val isOneToOne: Boolean get() = info().activeMembersCount == 2L
/**
* The live timeline of the room. Must be used to send Event to a room.
*/
@ -69,135 +50,14 @@ interface JoinedRoom : BaseRoom {
createTimelineParams: CreateTimelineParams,
): Result<Timeline>
suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit>
suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit>
suspend fun sendImage(
file: File,
thumbnailFile: File?,
imageInfo: ImageInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun sendVideo(
file: File,
thumbnailFile: File?,
videoInfo: VideoInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun sendAudio(
file: File,
audioInfo: AudioInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun sendFile(
file: File,
fileInfo: FileInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler>
/**
* Share a location message in the room.
*
* @param body A human readable textual representation of the location.
* @param geoUri A geo URI (RFC 5870) representing the location e.g. `geo:51.5008,0.1247;u=35`.
* Respectively: latitude, longitude, and (optional) uncertainty.
* @param description Optional description of the location to display to the user.
* @param zoomLevel Optional zoom level to display the map at.
* @param assetType Optional type of the location asset.
* Set to SENDER if sharing own location. Set to PIN if sharing any location.
*/
suspend fun sendLocation(
body: String,
geoUri: String,
description: String? = null,
zoomLevel: Int? = null,
assetType: AssetType? = null,
): Result<Unit>
/**
* Create a poll in the room.
*
* @param question The question to ask.
* @param answers The list of answers.
* @param maxSelections The maximum number of answers that can be selected.
* @param pollKind The kind of poll to create.
*/
suspend fun createPoll(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit>
/**
* Edit a poll in the room.
*
* @param pollStartId The event ID of the poll start event.
* @param question The question to ask.
* @param answers The list of answers.
* @param maxSelections The maximum number of answers that can be selected.
* @param pollKind The kind of poll to create.
*/
suspend fun editPoll(
pollStartId: EventId,
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit>
/**
* Send a response to a poll.
*
* @param pollStartId The event ID of the poll start event.
* @param answers The list of answer ids to send.
*/
suspend fun sendPollResponse(pollStartId: EventId, answers: List<String>): Result<Unit>
/**
* Ends a poll in the room.
*
* @param pollStartId The event ID of the poll start event.
* @param text Fallback text of the poll end event.
*/
suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit>
/**
* Send a typing notification.
* @param isTyping True if the user is typing, false otherwise.
*/
suspend fun typingNotice(isTyping: Boolean): Result<Unit>
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit>
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
suspend fun cancelSend(transactionId: TransactionId): Result<Unit>
suspend fun inviteUserById(id: UserId): Result<Unit>
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>

View file

@ -666,6 +666,12 @@ class RustMatrixClient(
}
}
override suspend fun canReportRoom(): Boolean = withContext(sessionDispatcher) {
runCatching {
innerClient.isReportRoomApiSupported()
}.getOrDefault(false)
}
private suspend fun File.getCacheSize(
includeCryptoDb: Boolean = false,
): Long = withContext(sessionDispatcher) {

View file

@ -187,14 +187,17 @@ class RustMatrixAuthenticationService @Inject constructor(
private var pendingOAuthAuthorizationData: OAuthAuthorizationData? = null
override suspend fun getOidcUrl(prompt: OidcPrompt): Result<OidcDetails> {
override suspend fun getOidcUrl(
prompt: OidcPrompt,
loginHint: String?,
): Result<OidcDetails> {
return withContext(coroutineDispatchers.io) {
runCatching {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val oAuthAuthorizationData = client.urlForOidc(
oidcConfiguration = oidcConfigurationProvider.get(),
prompt = prompt.toRustPrompt(),
loginHint = null,
loginHint = loginHint,
)
val url = oAuthAuthorizationData.loginUrl()
pendingOAuthAuthorizationData = oAuthAuthorizationData

View file

@ -12,4 +12,8 @@ import org.matrix.rustcomponents.sdk.QrCodeData as RustQrCodeData
class SdkQrCodeLoginData(
internal val rustQrCodeData: RustQrCodeData,
) : MatrixQrCodeLoginData
) : MatrixQrCodeLoginData {
override fun serverName(): String? {
return rustQrCodeData.serverName()
}
}

View file

@ -13,20 +13,11 @@ import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
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.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.IntentionalMention
@ -35,14 +26,11 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsStat
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.libraries.matrix.impl.core.RustSendHandle
@ -59,11 +47,8 @@ import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
@ -71,12 +56,10 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.DateDividerMode
import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener
import org.matrix.rustcomponents.sdk.KnockRequestsListener
import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType
import org.matrix.rustcomponents.sdk.TimelineConfiguration
import org.matrix.rustcomponents.sdk.TimelineFilter
@ -89,11 +72,9 @@ import org.matrix.rustcomponents.sdk.getElementCallRequiredPermissions
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import uniffi.matrix_sdk.RoomPowerLevelChanges
import java.io.File
import kotlin.coroutines.cancellation.CancellationException
import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange
import org.matrix.rustcomponents.sdk.KnockRequest as InnerKnockRequest
import org.matrix.rustcomponents.sdk.RoomInfo as InnerRoomInfo
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
class JoinedRustRoom(
@ -101,7 +82,6 @@ class JoinedRustRoom(
private val liveInnerTimeline: InnerTimeline,
private val notificationSettingsService: NotificationSettingsService,
private val coroutineDispatchers: CoroutineDispatchers,
private val roomInfoMapper: RoomInfoMapper,
private val systemClock: SystemClock,
private val roomContentForwarder: RoomContentForwarder,
private val featureFlagService: FeatureFlagService,
@ -112,14 +92,6 @@ class JoinedRustRoom(
override val syncUpdateFlow = MutableStateFlow(0L)
override val roomInfoFlow: StateFlow<io.element.android.libraries.matrix.api.room.RoomInfo> = mxCallbackFlow {
innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener {
override fun call(roomInfo: InnerRoomInfo) {
channel.trySend(roomInfoMapper.map(roomInfo))
}
})
}.stateIn(roomCoroutineScope, started = SharingStarted.Lazily, initialValue = baseRoom.info())
override val roomTypingMembersFlow: Flow<List<UserId>> = mxCallbackFlow {
val initial = emptyList<UserId>()
channel.trySend(initial)
@ -264,10 +236,6 @@ class JoinedRustRoom(
}
}
override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit> {
return liveTimeline.sendMessage(body, htmlBody, intentionalMentions)
}
override suspend fun editMessage(
eventId: EventId,
body: String,
@ -281,159 +249,12 @@ class JoinedRustRoom(
}
}
override suspend fun sendImage(
file: File,
thumbnailFile: File?,
imageInfo: ImageInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> {
return liveTimeline.sendImage(
file = file,
thumbnailFile = thumbnailFile,
imageInfo = imageInfo,
caption = caption,
formattedCaption = formattedCaption,
progressCallback = progressCallback,
replyParameters = replyParameters
)
}
override suspend fun sendVideo(
file: File,
thumbnailFile: File?,
videoInfo: VideoInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> {
return liveTimeline.sendVideo(
file = file,
thumbnailFile = thumbnailFile,
videoInfo = videoInfo,
caption = caption,
formattedCaption = formattedCaption,
progressCallback = progressCallback,
replyParameters = replyParameters
)
}
override suspend fun sendAudio(
file: File,
audioInfo: AudioInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> {
return liveTimeline.sendAudio(
file = file,
audioInfo = audioInfo,
caption = caption,
formattedCaption = formattedCaption,
progressCallback = progressCallback,
replyParameters = replyParameters,
)
}
override suspend fun sendFile(
file: File,
fileInfo: FileInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> {
return liveTimeline.sendFile(
file = file,
fileInfo = fileInfo,
caption = caption,
formattedCaption = formattedCaption,
progressCallback = progressCallback,
replyParameters = replyParameters,
)
}
override suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> {
return liveTimeline.sendVoiceMessage(
file = file,
audioInfo = audioInfo,
waveform = waveform,
progressCallback = progressCallback,
replyParameters = replyParameters,
)
}
override suspend fun sendLocation(
body: String,
geoUri: String,
description: String?,
zoomLevel: Int?,
assetType: AssetType?,
): Result<Unit> {
return liveTimeline.sendLocation(body, geoUri, description, zoomLevel, assetType)
}
override suspend fun createPoll(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit> {
return liveTimeline.createPoll(question, answers, maxSelections, pollKind)
}
override suspend fun editPoll(
pollStartId: EventId,
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit> {
return liveTimeline.editPoll(pollStartId, question, answers, maxSelections, pollKind)
}
override suspend fun sendPollResponse(
pollStartId: EventId,
answers: List<String>
): Result<Unit> {
return liveTimeline.sendPollResponse(pollStartId, answers)
}
override suspend fun endPoll(
pollStartId: EventId,
text: String
): Result<Unit> {
return liveTimeline.endPoll(pollStartId, text)
}
override suspend fun typingNotice(isTyping: Boolean) = withContext(roomDispatcher) {
runCatching {
innerRoom.typingNotice(isTyping)
}
}
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> {
return liveTimeline.toggleReaction(emoji, eventOrTransactionId)
}
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> {
return liveTimeline.forwardEvent(eventId, roomIds)
}
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> {
return liveTimeline.cancelSend(transactionId)
}
override suspend fun inviteUserById(id: UserId): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.inviteUserById(id.value)
@ -645,7 +466,6 @@ class JoinedRustRoom(
override fun destroy() {
baseRoom.destroy()
liveInnerTimeline.destroy()
roomCoroutineScope.cancel()
}
private fun InnerTimeline.map(

View file

@ -31,10 +31,14 @@ import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper
import io.element.android.libraries.matrix.impl.roomdirectory.map
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import uniffi.matrix_sdk_base.EncryptionState
@ -48,6 +52,7 @@ class RustBaseRoom(
private val roomSyncSubscriber: RoomSyncSubscriber,
private val roomMembershipObserver: RoomMembershipObserver,
sessionCoroutineScope: CoroutineScope,
roomInfoMapper: RoomInfoMapper,
initialRoomInfo: RoomInfo,
) : BaseRoom {
override val roomId = RoomId(innerRoom.id())
@ -62,10 +67,16 @@ class RustBaseRoom(
override val membersStateFlow: StateFlow<RoomMembersState> = roomMemberListFetcher.membersFlow
override val roomInfoFlow: StateFlow<RoomInfo> = MutableStateFlow(initialRoomInfo)
override val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
override val roomInfoFlow: StateFlow<RoomInfo> = mxCallbackFlow {
innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener {
override fun call(roomInfo: org.matrix.rustcomponents.sdk.RoomInfo) {
channel.trySend(roomInfoMapper.map(roomInfo))
}
})
}.stateIn(roomCoroutineScope, started = SharingStarted.Lazily, initialValue = initialRoomInfo)
override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId)
override suspend fun updateMembers() {
@ -98,6 +109,7 @@ class RustBaseRoom(
override fun destroy() {
innerRoom.destroy()
roomCoroutineScope.cancel()
}
override suspend fun userDisplayName(userId: UserId): Result<String?> = withContext(roomDispatcher) {

View file

@ -9,7 +9,6 @@ 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.data.tryOrNull
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.RoomId
@ -21,6 +20,7 @@ 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.preview.RoomPreviewInfoMapper
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
@ -30,10 +30,10 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import org.matrix.rustcomponents.sdk.Room as SdkRoom
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
class RustRoomFactory(
@ -56,11 +56,6 @@ class RustRoomFactory(
private val mutex = Mutex()
private val isDestroyed: AtomicBoolean = AtomicBoolean(false)
private data class RustRoomReferences(
val roomListItem: RoomListItem,
val room: SdkRoom,
)
private val roomInfoMapper = RoomInfoMapper()
private val eventFilters = TimelineConfig.excludedEvents
@ -84,20 +79,26 @@ class RustRoomFactory(
Timber.d("Room factory is destroyed, returning null for $roomId")
return@withContext null
}
val roomReferences = awaitRoomReferences(roomId) ?: return@withContext null
getBaseRoom(roomReferences)
val roomListItem = awaitRoomListItem(roomId) ?: return@withContext null
getBaseRoom(roomListItem)
}
}
private suspend fun getBaseRoom(roomReferences: RustRoomReferences): RustBaseRoom? {
val initialRoomInfo = roomReferences.room.roomInfo()
private suspend fun getBaseRoom(roomListItem: RoomListItem): RustBaseRoom? {
val sdkRoom = innerClient.getRoom(roomListItem.id()) ?: return null
return getBaseRoom(sdkRoom)
}
private suspend fun getBaseRoom(sdkRoom: Room): RustBaseRoom {
val initialRoomInfo = sdkRoom.roomInfo()
return RustBaseRoom(
sessionId = sessionId,
deviceId = deviceId,
innerRoom = roomReferences.room,
innerRoom = sdkRoom,
coroutineDispatchers = dispatchers,
roomSyncSubscriber = roomSyncSubscriber,
roomMembershipObserver = roomMembershipObserver,
roomInfoMapper = roomInfoMapper,
initialRoomInfo = roomInfoMapper.map(initialRoomInfo),
sessionCoroutineScope = sessionCoroutineScope,
)
@ -109,31 +110,26 @@ class RustRoomFactory(
Timber.d("Room factory is destroyed, returning null for $roomId")
return@withContext null
}
val roomReferences = awaitRoomReferences(roomId) ?: return@withContext null
if (roomReferences.room.membership() == Membership.JOINED) {
val baseRoom = getBaseRoom(roomReferences) ?: return@withContext null
val roomListItem = awaitRoomListItem(roomId) ?: return@withContext null
if (roomListItem.membership() == Membership.JOINED) {
// Init the live timeline in the SDK from the RoomListItem
if (!roomReferences.roomListItem.isTimelineInitialized()) {
roomReferences.roomListItem.initTimeline(eventFilters, "LIVE")
}
val sdkRoom = roomListItem.fullRoomWithTimeline(eventFilters)
GetRoomResult.Joined(
JoinedRustRoom(
baseRoom = baseRoom,
baseRoom = getBaseRoom(sdkRoom),
notificationSettingsService = notificationSettingsService,
roomContentForwarder = roomContentForwarder,
liveInnerTimeline = roomReferences.room.timeline(),
liveInnerTimeline = sdkRoom.timeline(),
coroutineDispatchers = dispatchers,
systemClock = systemClock,
roomInfoMapper = roomInfoMapper,
featureFlagService = featureFlagService,
)
)
} else {
val preview = try {
roomReferences.roomListItem.previewRoom(via = emptyList())
roomListItem.previewRoom(via = emptyList())
} catch (e: Exception) {
Timber.e(e, "Failed to get room preview for $roomId")
return@withContext null
@ -142,7 +138,7 @@ class RustRoomFactory(
GetRoomResult.NotJoined(
NotJoinedRustRoom(
sessionId = sessionId,
localRoom = getBaseRoom(roomReferences),
localRoom = getBaseRoom(roomListItem),
previewInfo = RoomPreviewInfoMapper.map(preview.info()),
)
)
@ -150,36 +146,23 @@ class RustRoomFactory(
}
}
private fun getRoomReferences(roomId: RoomId): RustRoomReferences? {
val roomListItem = innerRoomListService.roomOrNull(roomId.value)
/**
* Get the Rust room list item for a room, retrying after the room list is loaded if necessary.
*/
private suspend fun awaitRoomListItem(roomId: RoomId): RoomListItem? {
var roomListItem = innerRoomListService.roomOrNull(roomId.value)
if (roomListItem == null) {
// ... otherwise, lets wait for the SS to load all rooms and check again.
roomListService.allRooms.awaitLoaded()
roomListItem = innerRoomListService.roomOrNull(roomId.value)
}
if (roomListItem == null) {
Timber.d("Room not found for $roomId")
return null
}
val room = tryOrNull {
innerClient.getRoom(roomId.value)
} ?: error("Failed to get room for room id: $roomId")
Timber.d("Got room for $roomId")
return RustRoomReferences(
roomListItem = roomListItem,
room = room,
)
}
/**
* Get the Rust room references for a room, retrying after the room list is loaded if necessary.
*/
private suspend fun awaitRoomReferences(roomId: RoomId): RustRoomReferences? {
var roomReferences = getRoomReferences(roomId)
if (roomReferences == null) {
// ... otherwise, lets wait for the SS to load all rooms and check again.
roomListService.allRooms.awaitLoaded()
roomReferences = getRoomReferences(roomId)
}
return roomReferences
return roomListItem
}
}

View file

@ -49,6 +49,7 @@ class DefaultCallWidgetSettingsProvider @Inject constructor(
sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) "RELEASE" else "DEBUG",
parentUrl = null,
hideHeader = true,
controlledMediaDevices = false,
)
val rustWidgetSettings = newVirtualElementCallWidget(options)
return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings)

View file

@ -0,0 +1,35 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.auth.qrlogin
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeQrCodeData
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import org.junit.Test
class SdkQrCodeLoginDataTest {
@Test
fun `getServer reads the value from the Rust side, null case`() {
val sut = SdkQrCodeLoginData(
rustQrCodeData = FakeQrCodeData(
serverNameResult = { null },
),
)
assertThat(sut.serverName()).isNull()
}
@Test
fun `getServer reads the value from the Rust side`() {
val sut = SdkQrCodeLoginData(
rustQrCodeData = FakeQrCodeData(
serverNameResult = { A_HOMESERVER_URL },
),
)
assertThat(sut.serverName()).isEqualTo(A_HOMESERVER_URL)
}
}

View file

@ -0,0 +1,20 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.fakes
import io.element.android.tests.testutils.lambda.lambdaError
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.QrCodeData
class FakeQrCodeData(
private val serverNameResult: () -> String? = { lambdaError() },
) : QrCodeData(NoPointer) {
override fun serverName(): String? {
return serverNameResult()
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class RustBaseRoomTest {
@Test
fun `RustBaseRoom should cancel the room coroutine scope when it is destroyed`() = runTest {
val rustBaseRoom = createRustBaseRoom(
// Not using backgroundScope here, but the test scope
sessionCoroutineScope = this
)
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isTrue()
rustBaseRoom.destroy()
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isFalse()
}
private fun TestScope.createRustBaseRoom(
sessionCoroutineScope: CoroutineScope,
): RustBaseRoom {
val dispatchers = testCoroutineDispatchers()
return RustBaseRoom(
sessionId = A_SESSION_ID,
deviceId = A_DEVICE_ID,
innerRoom = FakeRustRoom(),
coroutineDispatchers = dispatchers,
roomSyncSubscriber = RoomSyncSubscriber(
roomListService = FakeRustRoomListService(),
dispatchers = dispatchers,
),
roomMembershipObserver = RoomMembershipObserver(),
sessionCoroutineScope = sessionCoroutineScope,
roomInfoMapper = RoomInfoMapper(),
initialRoomInfo = aRoomInfo(),
)
}
}

View file

@ -88,6 +88,7 @@ class FakeMatrixClient(
private val availableSlidingSyncVersionsLambda: () -> Result<List<SlidingSyncVersion>> = { lambdaError() },
private val ignoreUserResult: (UserId) -> Result<Unit> = { lambdaError() },
private var unIgnoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
private val canReportRoomLambda: () -> Boolean = { false },
override val ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf()),
) : MatrixClient {
var setDisplayNameCalled: Boolean = false
@ -329,4 +330,8 @@ class FakeMatrixClient(
override suspend fun availableSlidingSyncVersions(): Result<List<SlidingSyncVersion>> {
return availableSlidingSyncVersionsLambda()
}
override suspend fun canReportRoom(): Boolean {
return canReportRoomLambda()
}
}

View file

@ -19,7 +19,6 @@ 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
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
const val A_USER_NAME = "alice"
const val A_USER_NAME_2 = "Bob"
@ -73,7 +72,6 @@ const val A_HOMESERVER_URL_2 = "matrix-client.org"
val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false)
val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE
val A_ROOM_NOTIFICATION_SETTINGS = RoomNotificationSettings(mode = A_ROOM_NOTIFICATION_MODE, isDefault = false)
const val AN_AVATAR_URL = "mxc://data"
@ -88,3 +86,5 @@ val A_SERVER_LIST = listOf("server1", "server2")
const val A_TIMESTAMP = 567L
const val A_FORMATTED_DATE = "April 6, 1980 at 6:35 PM"
const val A_LOGIN_HINT = "mxid:@alice:example.org"

View file

@ -87,7 +87,10 @@ class FakeMatrixAuthenticationService(
return importCreatedSessionLambda(externalSession)
}
override suspend fun getOidcUrl(prompt: OidcPrompt): Result<OidcDetails> = simulateLongTask {
override suspend fun getOidcUrl(
prompt: OidcPrompt,
loginHint: String?,
): Result<OidcDetails> = simulateLongTask {
oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA)
}

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.test.auth.qrlogin
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
class FakeMatrixQrCodeLoginDataFactory(
@ -20,4 +21,8 @@ class FakeMatrixQrCodeLoginDataFactory(
}
}
class FakeMatrixQrCodeLoginData : MatrixQrCodeLoginData
class FakeMatrixQrCodeLoginData(
private val serverNameResult: () -> String? = { lambdaError() },
) : MatrixQrCodeLoginData {
override fun serverName() = serverNameResult()
}

View file

@ -12,17 +12,9 @@ import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
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.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.IntentionalMention
@ -33,16 +25,12 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsStat
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.tests.testutils.lambda.lambdaError
@ -53,7 +41,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.TestScope
import java.io.File
class FakeJoinedRoom(
val baseRoom: FakeBaseRoom = FakeBaseRoom(),
@ -63,35 +50,16 @@ class FakeJoinedRoom(
override val roomTypingMembersFlow: Flow<List<UserId>> = MutableStateFlow(emptyList()),
override val identityStateChangesFlow: Flow<List<IdentityStateChange>> = MutableStateFlow(emptyList()),
override val roomNotificationSettingsStateFlow: StateFlow<RoomNotificationSettingsState> =
MutableStateFlow(RoomNotificationSettingsState.Unknown),
MutableStateFlow(RoomNotificationSettingsState.Unknown),
override val knockRequestsFlow: Flow<List<KnockRequest>> = MutableStateFlow(emptyList()),
private val roomNotificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
private var createTimelineResult: (CreateTimelineParams) -> Result<Timeline> = { lambdaError() },
private val sendMessageResult: (String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _ -> lambdaError() },
private val editMessageLambda: (EventId, String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _, _ -> lambdaError() },
private val sendImageResult: (File, File?, ImageInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _, _, _ -> lambdaError() },
private val sendVideoResult: (File, File?, VideoInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _, _, _ -> lambdaError() },
private val sendFileResult: (File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _, _ -> lambdaError() },
private val sendAudioResult: (File, AudioInfo, String?, String?, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _, _ -> lambdaError() },
private val sendVoiceMessageResult: (File, AudioInfo, List<Float>, ProgressCallback?, ReplyParameters?) -> Result<FakeMediaUploadHandler> =
{ _, _, _, _, _ -> lambdaError() },
private val sendLocationResult: (String, String, String?, Int?, AssetType?) -> Result<Unit> = { _, _, _, _, _ -> lambdaError() },
private val sendCallNotificationIfNeededResult: () -> Result<Unit> = { lambdaError() },
private val progressCallbackValues: List<Pair<Long, Long>> = emptyList(),
private val createPollResult: (String, List<String>, Int, PollKind) -> Result<Unit> = { _, _, _, _ -> lambdaError() },
private val editPollResult: (EventId, String, List<String>, Int, PollKind) -> Result<Unit> = { _, _, _, _, _ -> lambdaError() },
private val sendPollResponseResult: (EventId, List<String>) -> Result<Unit> = { _, _ -> lambdaError() },
private val endPollResult: (EventId, String) -> Result<Unit> = { _, _ -> lambdaError() },
private val generateWidgetWebViewUrlResult: (MatrixWidgetSettings, String, String?, String?) -> Result<String> = { _, _, _, _ -> lambdaError() },
private val getWidgetDriverResult: (MatrixWidgetSettings) -> Result<MatrixWidgetDriver> = { lambdaError() },
private val typingNoticeResult: (Boolean) -> Result<Unit> = { lambdaError() },
private val toggleReactionResult: (String, EventOrTransactionId) -> Result<Unit> = { _, _ -> lambdaError() },
private val forwardEventResult: (EventId, List<RoomId>) -> Result<Unit> = { _, _ -> lambdaError() },
private val cancelSendResult: (TransactionId) -> Result<Unit> = { lambdaError() },
private val inviteUserResult: (UserId) -> Result<Unit> = { lambdaError() },
private val setNameResult: (String) -> Result<Unit> = { lambdaError() },
private val setTopicResult: (String) -> Result<Unit> = { lambdaError() },
@ -127,10 +95,6 @@ class FakeJoinedRoom(
createTimelineResult(createTimelineParams)
}
override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit> = simulateLongTask {
sendMessageResult(body, htmlBody, intentionalMentions)
}
override suspend fun editMessage(
eventId: EventId,
body: String,
@ -140,174 +104,10 @@ class FakeJoinedRoom(
editMessageLambda(eventId, body, htmlBody, intentionalMentions)
}
override suspend fun sendImage(
file: File,
thumbnailFile: File?,
imageInfo: ImageInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendImageResult(
file,
thumbnailFile,
imageInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
override suspend fun sendVideo(
file: File,
thumbnailFile: File?,
videoInfo: VideoInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendVideoResult(
file,
thumbnailFile,
videoInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
override suspend fun sendAudio(
file: File,
audioInfo: AudioInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendAudioResult(
file,
audioInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
override suspend fun sendFile(
file: File,
fileInfo: FileInfo,
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendFileResult(
file,
fileInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
override suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Float>,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendVoiceMessageResult(
file,
audioInfo,
waveform,
progressCallback,
replyParameters,
)
}
override suspend fun sendLocation(
body: String,
geoUri: String,
description: String?,
zoomLevel: Int?,
assetType: AssetType?,
): Result<Unit> = simulateLongTask {
return sendLocationResult(
body,
geoUri,
description,
zoomLevel,
assetType,
)
}
override suspend fun createPoll(question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind): Result<Unit> = simulateLongTask {
return createPollResult(
question,
answers,
maxSelections,
pollKind,
)
}
override suspend fun editPoll(
pollStartId: EventId,
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind
): Result<Unit> = simulateLongTask {
return editPollResult(
pollStartId,
question,
answers,
maxSelections,
pollKind,
)
}
override suspend fun sendPollResponse(pollStartId: EventId, answers: List<String>): Result<Unit> = simulateLongTask {
return sendPollResponseResult(
pollStartId,
answers,
)
}
override suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit> = simulateLongTask {
endPollResult(
pollStartId,
text,
)
}
override suspend fun typingNotice(isTyping: Boolean): Result<Unit> = simulateLongTask {
typingNoticeResult(isTyping)
}
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = simulateLongTask {
toggleReactionResult(emoji, eventOrTransactionId)
}
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = simulateLongTask {
forwardEventResult(eventId, roomIds)
}
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = simulateLongTask {
cancelSendResult(transactionId)
}
override suspend fun inviteUserById(id: UserId): Result<Unit> = simulateLongTask {
inviteUserResult(id)
}

View file

@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.test.timeline
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
@ -26,6 +27,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransa
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -47,23 +50,31 @@ class FakeTimeline(
)
),
override val membershipChangeEventReceived: Flow<Unit> = MutableSharedFlow(),
private val progressCallbackValues: List<Pair<Long, Long>> = emptyList(),
private val cancelSendResult: (TransactionId) -> Result<Unit> = { lambdaError() },
) : Timeline {
var sendMessageLambda: (
body: String,
htmlBody: String?,
intentionalMentions: List<IntentionalMention>,
) -> Result<Unit> = { _, _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = simulateLongTask {
cancelSendResult(transactionId)
}
override suspend fun sendMessage(
body: String,
htmlBody: String?,
intentionalMentions: List<IntentionalMention>,
): Result<Unit> = sendMessageLambda(body, htmlBody, intentionalMentions)
): Result<Unit> = simulateLongTask {
sendMessageLambda(body, htmlBody, intentionalMentions)
}
var redactEventLambda: (eventOrTransactionId: EventOrTransactionId, reason: String?) -> Result<Unit> = { _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun redactEvent(
@ -77,7 +88,7 @@ class FakeTimeline(
htmlBody: String?,
intentionalMentions: List<IntentionalMention>,
) -> Result<Unit> = { _, _, _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun editMessage(
@ -117,7 +128,7 @@ class FakeTimeline(
intentionalMentions: List<IntentionalMention>,
fromNotification: Boolean,
) -> Result<Unit> = { _, _, _, _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun replyMessage(
@ -154,15 +165,18 @@ class FakeTimeline(
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendImageLambda(
file,
thumbnailFile,
imageInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendImageLambda(
file,
thumbnailFile,
imageInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
var sendVideoLambda: (
file: File,
@ -184,15 +198,18 @@ class FakeTimeline(
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendVideoLambda(
file,
thumbnailFile,
videoInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendVideoLambda(
file,
thumbnailFile,
videoInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
var sendAudioLambda: (
file: File,
@ -212,14 +229,17 @@ class FakeTimeline(
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendAudioLambda(
file,
audioInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendAudioLambda(
file,
audioInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
var sendFileLambda: (
file: File,
@ -239,14 +259,17 @@ class FakeTimeline(
formattedCaption: String?,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendFileLambda(
file,
fileInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendFileLambda(
file,
fileInfo,
caption,
formattedCaption,
progressCallback,
replyParameters,
)
}
var sendVoiceMessageLambda: (
file: File,
@ -264,13 +287,16 @@ class FakeTimeline(
waveform: List<Float>,
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<MediaUploadHandler> = sendVoiceMessageLambda(
file,
audioInfo,
waveform,
progressCallback,
replyParameters,
)
): Result<MediaUploadHandler> = simulateLongTask {
simulateSendMediaProgress(progressCallback)
sendVoiceMessageLambda(
file,
audioInfo,
waveform,
progressCallback,
replyParameters,
)
}
var sendLocationLambda: (
body: String,
@ -279,7 +305,7 @@ class FakeTimeline(
zoomLevel: Int?,
assetType: AssetType?,
) -> Result<Unit> = { _, _, _, _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun sendLocation(
@ -288,24 +314,30 @@ class FakeTimeline(
description: String?,
zoomLevel: Int?,
assetType: AssetType?,
): Result<Unit> = sendLocationLambda(
body,
geoUri,
description,
zoomLevel,
assetType
)
): Result<Unit> = simulateLongTask {
sendLocationLambda(
body,
geoUri,
description,
zoomLevel,
assetType,
)
}
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Unit> = { _, _ -> Result.success(Unit) }
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Unit> = { _, _ -> lambdaError() }
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = toggleReactionLambda(
emoji,
eventOrTransactionId
)
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = simulateLongTask {
toggleReactionLambda(
emoji,
eventOrTransactionId,
)
}
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> lambdaError() }
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = forwardEventLambda(eventId, roomIds)
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = simulateLongTask {
forwardEventLambda(eventId, roomIds)
}
var createPollLambda: (
question: String,
@ -313,20 +345,17 @@ class FakeTimeline(
maxSelections: Int,
pollKind: PollKind,
) -> Result<Unit> = { _, _, _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun createPoll(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit> = createPollLambda(
question,
answers,
maxSelections,
pollKind
)
override suspend fun createPoll(question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind): Result<Unit> = simulateLongTask {
createPollLambda(
question,
answers,
maxSelections,
pollKind,
)
}
var editPollLambda: (
pollStartId: EventId,
@ -335,7 +364,7 @@ class FakeTimeline(
maxSelections: Int,
pollKind: PollKind,
) -> Result<Unit> = { _, _, _, _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun editPoll(
@ -343,44 +372,56 @@ class FakeTimeline(
question: String,
answers: List<String>,
maxSelections: Int,
pollKind: PollKind,
): Result<Unit> = editPollLambda(
pollStartId,
question,
answers,
maxSelections,
pollKind
)
pollKind: PollKind
): Result<Unit> = simulateLongTask {
editPollLambda(
pollStartId,
question,
answers,
maxSelections,
pollKind,
)
}
var sendPollResponseLambda: (
pollStartId: EventId,
answers: List<String>,
) -> Result<Unit> = { _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun sendPollResponse(
pollStartId: EventId,
answers: List<String>,
): Result<Unit> = sendPollResponseLambda(pollStartId, answers)
): Result<Unit> = simulateLongTask {
sendPollResponseLambda(
pollStartId,
answers,
)
}
var endPollLambda: (
pollStartId: EventId,
text: String,
) -> Result<Unit> = { _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun endPoll(
pollStartId: EventId,
text: String,
): Result<Unit> = endPollLambda(pollStartId, text)
): Result<Unit> = simulateLongTask {
endPollLambda(
pollStartId,
text,
)
}
var sendReadReceiptLambda: (
eventId: EventId,
receiptType: ReceiptType,
) -> Result<Unit> = { _, _ ->
Result.success(Unit)
lambdaError()
}
override suspend fun sendReadReceipt(
@ -417,5 +458,12 @@ class FakeTimeline(
closeCounter++
}
private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) {
progressCallbackValues.forEach { (current, total) ->
progressCallback?.onProgress(current, total)
delay(1)
}
}
override fun toString() = "FakeTimeline: $name"
}

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
@ -49,7 +50,7 @@ class MediaSender @Inject constructor(
progressCallback: ProgressCallback?,
replyParameters: ReplyParameters?,
): Result<Unit> {
return room.sendMedia(
return room.liveTimeline.sendMedia(
uploadInfo = mediaUploadInfo,
progressCallback = progressCallback,
caption = caption,
@ -76,7 +77,7 @@ class MediaSender @Inject constructor(
compressIfPossible = compressIfPossible,
)
.flatMapCatching { info ->
room.sendMedia(
room.liveTimeline.sendMedia(
uploadInfo = info,
progressCallback = progressCallback,
caption = caption,
@ -108,7 +109,7 @@ class MediaSender @Inject constructor(
audioInfo = audioInfo,
waveform = waveForm,
)
room.sendMedia(
room.liveTimeline.sendMedia(
uploadInfo = newInfo,
progressCallback = progressCallback,
caption = null,
@ -130,7 +131,7 @@ class MediaSender @Inject constructor(
ongoingUploadJobs.remove(Job)
}
private suspend fun JoinedRoom.sendMedia(
private suspend fun Timeline.sendMedia(
uploadInfo: MediaUploadInfo,
progressCallback: ProgressCallback?,
caption: String?,

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.message.ReplyParameters
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
@ -51,7 +52,9 @@ class MediaSenderTest {
Result.success(FakeMediaUploadHandler())
}
val room = FakeJoinedRoom(
sendImageResult = sendImageResult
liveTimeline = FakeTimeline().apply {
sendImageLambda = sendImageResult
},
)
val sender = createMediaSender(room = room)
@ -74,14 +77,22 @@ class MediaSenderTest {
@Test
fun `given a failure in the media upload when sending the whole process fails`() = runTest {
val preProcessor = FakeMediaPreProcessor().apply {
givenImageResult()
}
val sendImageResult =
lambdaRecorder { _: File, _: File?, _: ImageInfo, _: String?, _: String?, _: ProgressCallback?, _: ReplyParameters? ->
Result.failure<FakeMediaUploadHandler>(Exception())
}
val room = FakeJoinedRoom(
sendImageResult = sendImageResult
liveTimeline = FakeTimeline().apply {
sendImageLambda = sendImageResult
},
)
val sender = createMediaSender(
preProcessor = preProcessor,
room = room,
)
val sender = createMediaSender(room = room)
val uri = Uri.parse("content://image.jpg")
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
@ -94,10 +105,12 @@ class MediaSenderTest {
fun `given a cancellation in the media upload when sending the job is cancelled`() = runTest(StandardTestDispatcher()) {
val sendFileResult =
lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, ReplyParameters?, Result<FakeMediaUploadHandler>> { _, _, _, _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
Result.success(FakeMediaUploadHandler())
}
val room = FakeJoinedRoom(
sendFileResult = sendFileResult
liveTimeline = FakeTimeline().apply {
sendFileLambda = sendFileResult
},
)
val sender = createMediaSender(room = room)
val sendJob = launch {

View file

@ -5,35 +5,11 @@
<string name="notification_channel_noisy">"Hysbysiadau swnllyd"</string>
<string name="notification_channel_ringing_calls">"Galwadau\'n canu"</string>
<string name="notification_channel_silent">"Hysbysiadau tawel"</string>
<plurals name="notification_compat_summary_line_for_room">
<item quantity="zero">"%1$s: %2$d negeseuon"</item>
<item quantity="one">"%1$s: %2$d neges"</item>
<item quantity="two">"%1$s: %2$d neges"</item>
<item quantity="few">"%1$s: %2$d neges"</item>
<item quantity="many">"%1$s: %2$d neges"</item>
<item quantity="other">"%1$s: %2$d neges"</item>
</plurals>
<plurals name="notification_compat_summary_title">
<item quantity="zero">"%d hysbysiadau"</item>
<item quantity="one">"%d hysbysiad"</item>
<item quantity="two">"%d hysbysiad"</item>
<item quantity="few">"%d hysbysiad"</item>
<item quantity="many">"%d hysbysiad"</item>
<item quantity="other">"%d hysbysiad"</item>
</plurals>
<string name="notification_fallback_content">"Hysbysiad"</string>
<string name="notification_incoming_call">"📹 Galwad i mewn"</string>
<string name="notification_inline_reply_failed">"** Wedi methu anfon - agorwch yr ystafell"</string>
<string name="notification_invitation_action_join">"Ymuno"</string>
<string name="notification_invitation_action_reject">"Gwrthod"</string>
<plurals name="notification_invitations">
<item quantity="zero">"%d gwahoddiadau"</item>
<item quantity="one">"%d gwahoddiadau"</item>
<item quantity="two">"%d gwahoddiadau"</item>
<item quantity="few">"%d gwahoddiadau"</item>
<item quantity="many">"%d gwahoddiadau"</item>
<item quantity="other">"%d gwahoddiadau"</item>
</plurals>
<string name="notification_invite_body">"Wedi eich gwahodd i sgwrsio"</string>
<string name="notification_invite_body_with_sender">"Mae %1$s wedi eich gwahodd i sgwrsio"</string>
<string name="notification_mentioned_you_body">"Wedi eich crybwyll: %1$s"</string>
@ -56,25 +32,9 @@
<string name="notification_test_push_notification_content">"Rydych chi\'n edrych ar yr hysbysiad! Cliciwch fi!"</string>
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
<string name="notification_ticker_text_group">"%1$s : %2$s %3$s"</string>
<plurals name="notification_unread_notified_messages">
<item quantity="zero">"%d negeseuon hysbyswyd heb eu darllen"</item>
<item quantity="one">"%d neges hysbyswyd heb ei ddarllen"</item>
<item quantity="two">"%d neges hysbyswyd heb eu darllen"</item>
<item quantity="few">"%d neges hysbyswyd heb eu darllen"</item>
<item quantity="many">"%d neges hysbyswyd heb eu darllen"</item>
<item quantity="other">"%d neges hysbyswyd heb eu darllen"</item>
</plurals>
<string name="notification_unread_notified_messages_and_invitation">"%1$s a %2$s"</string>
<string name="notification_unread_notified_messages_in_room">"%1$s yn %2$s"</string>
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s yn %2$s a %3$s"</string>
<plurals name="notification_unread_notified_messages_in_room_rooms">
<item quantity="zero">"%d ystafelloedd"</item>
<item quantity="one">"%d ystafell"</item>
<item quantity="two">"%d ystafell"</item>
<item quantity="few">"%d ystafell"</item>
<item quantity="many">"%d ystafell"</item>
<item quantity="other">"%d ystafell"</item>
</plurals>
<string name="push_distributor_background_sync_android">"Cydweddu\'n y cefndir"</string>
<string name="push_distributor_firebase_android">"Gwasanaethau Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Heb ganfod Google Play Services dilys. Efallai fydd hysbysiadau ddim yn gweithio\'n iawn."</string>
@ -84,14 +44,6 @@
<string name="troubleshoot_notifications_test_current_push_provider_title">"Darparwr gwthio presennol"</string>
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Gwnewch yn siŵr fod y cais yn cefnogi o leiaf un darparwr gwthio."</string>
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Heb ganfod cefnogaeth darparwr gwthio."</string>
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
<item quantity="zero">"Wedi canfod %1$d darparwyr gwthio: %2$s"</item>
<item quantity="one">"Wedi canfod %1$d darparwr gwthio: %2$s"</item>
<item quantity="two">"Wedi canfod %1$d darparwr gwthio: %2$s"</item>
<item quantity="few">"Wedi canfod %1$d darparwr gwthio: %2$s"</item>
<item quantity="many">"Wedi canfod %1$d darparwr gwthio: %2$s"</item>
<item quantity="other">"Wedi canfod %1$d darparwr gwthio: %2$s"</item>
</plurals>
<string name="troubleshoot_notifications_test_detect_push_provider_success_2">"Adeiladwyd y rhaglen gyda chefnogaeth ar gyfer: %1$s"</string>
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Cefnogaeth darparwr gwthio"</string>
<string name="troubleshoot_notifications_test_display_notification_description">"Gwiriwch y gall y cais ddangos hysbysiad."</string>

View file

@ -366,7 +366,7 @@ class NotificationBroadcastReceiverHandlerTest {
roomId = A_ROOM_ID,
),
)
runCurrent()
advanceUntilIdle()
sendMessage.assertions()
.isCalledOnce()
.with(value(A_MESSAGE), value(null), value(emptyList<IntentionalMention>()))

View file

@ -2,13 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_unified_push_description">"Sicrhewch fod dosbarthwyr UnifiedPush ar gael."</string>
<string name="troubleshoot_notifications_test_unified_push_failure">"Heb ganfod dosbarthwyr gwthio."</string>
<plurals name="troubleshoot_notifications_test_unified_push_success">
<item quantity="zero">"Wedi canfod %1$d dosbarthwyr: %2$s"</item>
<item quantity="one">"Wedi canfod %1$d dosbarthwr: %2$s"</item>
<item quantity="two">"Wedi canfod %1$d dosbarthwr: %2$s"</item>
<item quantity="few">"Wedi canfod %1$d dosbarthwr: %2$s"</item>
<item quantity="many">"Wedi canfod %1$d dosbarthwr: %2$s"</item>
<item quantity="other">"Wedi canfod %1$d dosbarthwr: %2$s"</item>
</plurals>
<string name="troubleshoot_notifications_test_unified_push_title">"Gwiriwch UnifiedPush"</string>
</resources>

View file

@ -44,8 +44,8 @@ import kotlin.coroutines.suspendCoroutine
@Composable
fun QrCodeCameraView(
onScanQrCode: (ByteArray) -> Unit,
renderPreview: Boolean,
modifier: Modifier = Modifier,
renderPreview: Boolean = true,
) {
if (LocalInspectionMode.current) {
Box(
@ -62,9 +62,11 @@ fun QrCodeCameraView(
var cameraProvider by remember { mutableStateOf<ProcessCameraProvider?>(null) }
val previewUseCase = remember { Preview.Builder().build() }
var lastFrame by remember { mutableStateOf<Bitmap?>(null) }
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
val imageAnalysis = remember {
ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
}
LaunchedEffect(Unit) {
cameraProvider = localContext.getCameraProvider()

View file

@ -172,6 +172,11 @@
<string name="common_light">"Светлая"</string>
<string name="common_link_copied_to_clipboard">"Спасылка скапіравана ў буфер абмену"</string>
<string name="common_loading">"Загрузка…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d іншы"</item>
<item quantity="few">"%d іншыя"</item>
<item quantity="many">"%d іншых"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d удзельнік"</item>
<item quantity="few">"%1$d удзельнікі"</item>

View file

@ -125,6 +125,10 @@
<string name="common_light">"Светъл"</string>
<string name="common_link_copied_to_clipboard">"Връзката е копирана в клипборда"</string>
<string name="common_loading">"Зарежда се…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d друг"</item>
<item quantity="other">"%d други"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d член"</item>
<item quantity="other">"%1$d членове"</item>

View file

@ -200,6 +200,11 @@ Důvod: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Odkaz zkopírován do schránky"</string>
<string name="common_loading">"Načítání…"</string>
<string name="common_loading_more">"Načítání dalších…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d další"</item>
<item quantity="few">"%d další"</item>
<item quantity="other">"%d dalších"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d člen"</item>
<item quantity="few">"%1$d členové"</item>
@ -343,6 +348,23 @@ Opravdu chcete pokračovat?"</string>
<string name="invite_friends_text">"Ahoj, ozvi se mi na %1$s: %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Zatřeste zařízením pro nahlášení chyby"</string>
<string name="screen_bottom_sheet_manage_room_member_ban">"Odebrat a vykázat člena"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Vykázat"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Nebudou se moci znovu připojit k této místnosti, pokud budou pozváni."</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Jste si jisti, že chcete vykázat tohoto člena?"</string>
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Vykazování %1$s"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Odebrat"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Budou moci znovu vstoupit do této místnosti, pokud budou pozváni."</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Opravdu chcete tohoto člena odebrat?"</string>
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Zobrazit profil"</string>
<string name="screen_bottom_sheet_manage_room_member_remove">"Odebrat z místnosti"</string>
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Odebrat člena a zakázat mu připojení v budoucnu?"</string>
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Odstraňování %1$s…"</string>
<string name="screen_bottom_sheet_manage_room_member_unban">"Zrušit vykázání z místnosti"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Zrušit vykázání"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Pokud by byli pozváni, mohli by se znovu připojit do místnosti"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Opravdu chcete zrušit vykázání tohoto člena?"</string>
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Rušení vykázání %1$s"</string>
<string name="screen_media_picker_error_failed_selection">"Výběr média se nezdařil, zkuste to prosím znovu."</string>
<string name="screen_media_upload_preview_caption_warning">"Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>

View file

@ -2,14 +2,6 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_avatar">"Afatar"</string>
<string name="a11y_delete">"Dileu"</string>
<plurals name="a11y_digits_entered">
<item quantity="zero">"%1$d nodau wedi eu cynnig"</item>
<item quantity="one">"%1$d nod wedi ei gynnig"</item>
<item quantity="two">"%1$d nod wedi eu cynnig"</item>
<item quantity="few">"%1$d nod wedi eu cynnig"</item>
<item quantity="many">"%1$d nod wedi eu cynnig"</item>
<item quantity="other">"%1$d nod wedi eu cynnig"</item>
</plurals>
<string name="a11y_hide_password">"Cuddio cyfrinair"</string>
<string name="a11y_join_call">"Ymuno â galwad"</string>
<string name="a11y_jump_to_bottom">"Symud i\'r gwaelod"</string>
@ -25,14 +17,6 @@
<string name="a11y_react_with">"Ymateb gyda %1$s"</string>
<string name="a11y_react_with_other_emojis">"Ymateb gydag emojis eraill"</string>
<string name="a11y_read_receipts_multiple">"Wedi\'i ddarllen gan %1$s a %2$s"</string>
<plurals name="a11y_read_receipts_multiple_with_others">
<item quantity="zero">"Wedi\'i ddarllen gan %1$s a %2$d arall"</item>
<item quantity="one">"Wedi\'i ddarllen gan %1$s a %2$d arall"</item>
<item quantity="two">"Wedi\'i ddarllen gan %1$s a %2$d arall"</item>
<item quantity="few">"Wedi\'i ddarllen gan %1$s a %2$d arall"</item>
<item quantity="many">"Wedi\'i ddarllen gan %1$s a %2$d arall"</item>
<item quantity="other">"Wedi\'i ddarllen gan %1$s a %2$d arall"</item>
</plurals>
<string name="a11y_read_receipts_single">"Wedi\'i ddarllen gan %1$s"</string>
<string name="a11y_read_receipts_tap_to_show_all">"Tapio i ddangos y cyfan"</string>
<string name="a11y_remove_reaction_with">"Wedi dileu adwaith gyda %1$s"</string>
@ -206,14 +190,6 @@ Rheswm: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Dolen wedi\'i chopïo i\'r clipfwrdd"</string>
<string name="common_loading">"Yn Llwytho…"</string>
<string name="common_loading_more">"Wrthi\'n llwytho mwy…"</string>
<plurals name="common_member_count">
<item quantity="zero">"%1$d aelodau"</item>
<item quantity="one">"%1$d aelod"</item>
<item quantity="two">"%1$d aelod"</item>
<item quantity="few">"%1$d aelod"</item>
<item quantity="many">"%1$d aelod"</item>
<item quantity="other">"%1$d aelod"</item>
</plurals>
<string name="common_message">"Neges"</string>
<string name="common_message_actions">"Gweithredoedd neges"</string>
<string name="common_message_layout">"Cynllun y neges"</string>
@ -238,14 +214,6 @@ Rheswm: %1$s."</string>
<string name="common_poll_summary">"Pleidlais: %1$s"</string>
<string name="common_poll_total_votes">"Cyfanswm y pleidleisiau: %1$s"</string>
<string name="common_poll_undisclosed_text">"Bydd y canlyniadau\'n dangos ar ôl i\'r bleidlais ddod i ben"</string>
<plurals name="common_poll_votes_count">
<item quantity="zero">"%d pleidleisiau"</item>
<item quantity="one">"%d pleidlais"</item>
<item quantity="two">"%d pleidlais"</item>
<item quantity="few">"%d pleidlais"</item>
<item quantity="many">"%d pleidlais"</item>
<item quantity="other">"%d pleidlais"</item>
</plurals>
<string name="common_privacy_policy">"Polisi preifatrwydd"</string>
<string name="common_reaction">"Adwaith"</string>
<string name="common_reactions">"Adweithiau"</string>
@ -355,20 +323,29 @@ Ydych chi\'n siŵr eich bod am barhau?"</string>
<string name="invite_friends_text">"Hei, siaradwch â mi ar %1$s: %2$s"</string>
<string name="login_initial_device_name_android">"Android %1$s"</string>
<string name="preference_rageshake">"Rageshake i adrodd gwall"</string>
<string name="screen_bottom_sheet_manage_room_member_ban">"Gwahardd o ystafell"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Atal"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Fyddan nhw ddim yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad."</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Ydych chi\'n siŵr eich bod am wahardd yr aelod hwn?"</string>
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Yn gwahardd %1$s"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Tynnu"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Fyddan nhw yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad."</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Ydych chi\'n siŵr eich bod am ddileu\'r aelod hwn?"</string>
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Gweld proffil"</string>
<string name="screen_bottom_sheet_manage_room_member_remove">"Tynnu o\'r ystafell"</string>
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Dileu aelod a\'u gwahardd rhag ymuno yn y dyfodol?"</string>
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Wrthi\'n dileu %1$s…"</string>
<string name="screen_bottom_sheet_manage_room_member_unban">"Dad-wahardd o\'r ystafell"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Dad-wahardd"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Bydden nhw\'n gallu ymuno â\'r ystafell eto os fydd rhywun yn eu gwahodd"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Ydych chi\'n siŵr eich bod chi eisiau dadwahardd yr aelod hwn?"</string>
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Dad-wahardd %1$s"</string>
<string name="screen_media_picker_error_failed_selection">"Wedi methu dewis cyfrwng, ceisiwch eto."</string>
<string name="screen_media_upload_preview_caption_warning">"Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Wedi methu llwytho cyfryngau, ceisiwch eto."</string>
<string name="screen_pinned_timeline_empty_state_description">"Pwyswch ar neges a dewis “%1$s” i\'w cynnwys yma."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Pinio negeseuon pwysig fel y mae modd eu darganfod yn hawdd"</string>
<plurals name="screen_pinned_timeline_screen_title">
<item quantity="zero">"%1$d Negeseuon wedi\'u pinio"</item>
<item quantity="one">"%1$d Neges wedi\'i phinio"</item>
<item quantity="two">"%1$d Neges wedi\'i phinio"</item>
<item quantity="few">"%1$d Neges wedi\'i phinio"</item>
<item quantity="many">"%1$d Neges wedi\'i phinio"</item>
<item quantity="other">"%1$d Neges wedi\'i phinio"</item>
</plurals>
<string name="screen_pinned_timeline_screen_title_empty">"Negeseuon wedi\'u pinio"</string>
<string name="screen_reset_identity_confirmation_subtitle">"Rydych chi ar fin mynd i\'ch cyfrif %1$s i ailosod eich hunaniaeth. Wedi hynny byddwch yn cael eich tywys yn ôl i\'r ap."</string>
<string name="screen_reset_identity_confirmation_title">"Methu cadarnhau? Ewch i\'ch cyfrif i ailosod eich hunaniaeth."</string>

View file

@ -198,6 +198,10 @@ Grund: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Link in die Zwischenablage kopiert"</string>
<string name="common_loading">"Laden…"</string>
<string name="common_loading_more">"Mehr wird geladen…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d weitere"</item>
<item quantity="other">"%d weitere"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d Mitglied"</item>
<item quantity="other">"%1$d Mitglieder"</item>

View file

@ -198,6 +198,10 @@
<string name="common_link_copied_to_clipboard">"Ο σύνδεσμος αντιγράφηκε στο πρόχειρο"</string>
<string name="common_loading">"Φόρτωση…"</string>
<string name="common_loading_more">"Φόρτωση περισσότερων…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d ακόμη"</item>
<item quantity="other">"%d ακόμη"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d μέλος"</item>
<item quantity="other">"%1$d μέλη"</item>

View file

@ -185,6 +185,10 @@ Motivo: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Enlace copiado al portapapeles"</string>
<string name="common_loading">"Cargando…"</string>
<string name="common_loading_more">"Cargando más…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d otro"</item>
<item quantity="other">"%d otros"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d miembro"</item>
<item quantity="other">"%1$d miembros"</item>

View file

@ -198,6 +198,10 @@ Põhjus: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Link on kopeeritud lõikelauale"</string>
<string name="common_loading">"Laadime…"</string>
<string name="common_loading_more">"Laadime veel…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d muu"</item>
<item quantity="other">"%d muud"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d liige"</item>
<item quantity="other">"%1$d liiget"</item>
@ -339,6 +343,23 @@ Kas sa oled kindel, et soovid jätkata?"</string>
<string name="invite_friends_text">"Hei, suhtle minuga %1$s võrgus: %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Veast teatamiseks raputa nutiseadet ägedalt"</string>
<string name="screen_bottom_sheet_manage_room_member_ban">"Eemalda ja sea suhtluskeeld"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Sea suhtluskeeld"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Ta ei saa selle jututoaga liituda isegi kutse olemasolul."</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Kas sa oled kindel, et soovid sellele kasutajale seada suhtluskeelu?"</string>
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Seame kasutajale %1$s suhtluskeelu"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Eemalda"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Uue kutse saamisel on tal võimalik selle jututoaga uuesti liituda."</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Kas sa oled kindel, et soovid selle osaleja eemaldada?"</string>
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Vaata profiili"</string>
<string name="screen_bottom_sheet_manage_room_member_remove">"Eemalda kasutaja jututoast"</string>
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Kas eemaldama kasutaja ja seame talle tulevikuks suhtluskeelu?"</string>
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Eemaldame kasutajat %1$s…"</string>
<string name="screen_bottom_sheet_manage_room_member_unban">"Eemalda suhtluskeeld jututoas"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Eemalda suhtluskeeld"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Ta võib kutse saamisel liituda jututoaga uuesti"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Kas oled kindel, et soovid selle liikme suhtluskeelu eemaldada?"</string>
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Eemaldame suhtluskeelu kasutajalt %1$s"</string>
<string name="screen_media_picker_error_failed_selection">"Meediafaili valimine ei õnnestunud. Palun proovi uuesti."</string>
<string name="screen_media_upload_preview_caption_warning">"Selgitused ja alapealkirjad ei pruugi olla nähtavad vanemate rakenduste kasutajatele."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."</string>

View file

@ -188,6 +188,10 @@ Arrazoia: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Esteka arbelean kopiatu da"</string>
<string name="common_loading">"Kargatzen…"</string>
<string name="common_loading_more">"Gehiago kargatzen…"</string>
<plurals name="common_many_members">
<item quantity="one">"beste %d"</item>
<item quantity="other">"beste %d"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"Kide %1$d"</item>
<item quantity="other">"%1$d kide"</item>

View file

@ -198,6 +198,10 @@ Syy: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Linkki kopioitu leikepöydälle"</string>
<string name="common_loading">"Ladataan…"</string>
<string name="common_loading_more">"Ladataan lisää…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d muu"</item>
<item quantity="other">"%d muuta"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d jäsen"</item>
<item quantity="other">"%1$d jäsentä"</item>

View file

@ -198,6 +198,10 @@ Raison : %1$s."</string>
<string name="common_link_copied_to_clipboard">"Lien copié dans le presse-papiers"</string>
<string name="common_loading">"Chargement…"</string>
<string name="common_loading_more">"Chargement…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d autre"</item>
<item quantity="other">"%d autres"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d membre"</item>
<item quantity="other">"%1$d membres"</item>
@ -339,6 +343,23 @@ Raison : %1$s."</string>
<string name="invite_friends_text">"Salut, parle-moi sur %1$s : %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Rageshake pour signaler un problème"</string>
<string name="screen_bottom_sheet_manage_room_member_ban">"Retirer et bannir ce membre"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Bannir"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Il ne pourra pas rejoindre le salon à nouveau, même si il est invité."</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Êtes-vous certain de vouloir bannir ce membre ?"</string>
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Bannissement de %1$s"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Retirer"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Cet utilisateur pourra rejoindre le salon à nouveau si il est invité."</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Voulez-vous vraiment supprimer ce membre ?"</string>
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Voir le profil"</string>
<string name="screen_bottom_sheet_manage_room_member_remove">"Retirer le membre du salon"</string>
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Retirer le membre et interdire ladhésion à lavenir ?"</string>
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Enlever %1$s…"</string>
<string name="screen_bottom_sheet_manage_room_member_unban">"Débannir du salon"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Débannir"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Lutilisateur pourra à nouveau rejoindre le salon sil est invité."</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Êtes-vous sûr de vouloir débannir cet utilisateur?"</string>
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Débannissement de %1$s"</string>
<string name="screen_media_picker_error_failed_selection">"Échec de la sélection du média, veuillez réessayer."</string>
<string name="screen_media_upload_preview_caption_warning">"Les légendes peuvent ne pas être visibles pour les utilisateurs danciennes applications."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Échec du traitement des médias à télécharger, veuillez réessayer."</string>

View file

@ -198,6 +198,10 @@ Ok: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Hivatkozás a vágólapra másolva"</string>
<string name="common_loading">"Betöltés…"</string>
<string name="common_loading_more">"Továbbiak betöltése…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d további felhasználó"</item>
<item quantity="other">"%d további felhasználó"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d tag"</item>
<item quantity="other">"%1$d tag"</item>
@ -339,6 +343,23 @@ Biztos, hogy folytatja?"</string>
<string name="invite_friends_text">"Beszélgessünk itt: %1$s, %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Az eszköz rázása a hibajelentéshez"</string>
<string name="screen_bottom_sheet_manage_room_member_ban">"Eltávolítás és a tag kitiltása"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Kitiltás"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Többé nem csatlakozhat ehhez a szobához, akkor sem, ha meghívják."</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Biztos, hogy kitiltja ezt a tagot?"</string>
<string name="screen_bottom_sheet_manage_room_member_banning_user">"%1$s kitiltása"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Eltávolítás"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Ehhez a szobához is csatlakozhat, ha meghívják."</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Biztos, hogy eltávolítja ezt a tagot?"</string>
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Profil megtekintése"</string>
<string name="screen_bottom_sheet_manage_room_member_remove">"Eltávolítás a szobából"</string>
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Eltávolítja a tagot, és megtiltja a jövőbeni csatlakozást?"</string>
<string name="screen_bottom_sheet_manage_room_member_removing_user">"%1$s eltávolítása…"</string>
<string name="screen_bottom_sheet_manage_room_member_unban">"Visszaengedés a szobába"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Kitiltás visszavonása"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Újra beléphetnek a szobába, ha meghívják őket."</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Biztos, hogy feloldja a felhasználó kitiltását?"</string>
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"%1$s kitiltásának feloldása"</string>
<string name="screen_media_picker_error_failed_selection">"Nem sikerült kiválasztani a médiát, próbálja újra."</string>
<string name="screen_media_upload_preview_caption_warning">"Előfordulhat, hogy a feliratok nem láthatók a régebbi alkalmazásokat használók számára."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."</string>

View file

@ -181,6 +181,9 @@ Alasan: %1$s."</string>
<string name="common_light">"Terang"</string>
<string name="common_link_copied_to_clipboard">"Tautan disalin ke papan klip"</string>
<string name="common_loading">"Memuat…"</string>
<plurals name="common_many_members">
<item quantity="other">"%d lainnya"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="other">"%1$d anggota"</item>
</plurals>

View file

@ -190,6 +190,10 @@ Motivo:. %1$s"</string>
<string name="common_link_copied_to_clipboard">"Collegamento copiato negli appunti"</string>
<string name="common_loading">"Caricamento…"</string>
<string name="common_loading_more">"Caricamento in corso…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d altro"</item>
<item quantity="other">"altri %d"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d membro"</item>
<item quantity="other">"%1$d membri"</item>

View file

@ -147,6 +147,10 @@
<string name="common_light">"ღია"</string>
<string name="common_link_copied_to_clipboard">"ბმული კოპირებულია გაცვლის ბუფერში"</string>
<string name="common_loading">"იტვირთება…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d სხვა"</item>
<item quantity="other">"%d სხვა"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d წევრი"</item>
<item quantity="other">"%1$d წევრები"</item>

View file

@ -198,6 +198,10 @@
<string name="common_link_copied_to_clipboard">"Lenke kopiert til utklippstavlen"</string>
<string name="common_loading">"Laster inn…"</string>
<string name="common_loading_more">"Laster inn mer…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d andre"</item>
<item quantity="other">"%d andre"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d medlem"</item>
<item quantity="other">"%1$d medlemmer"</item>
@ -339,6 +343,23 @@ Er du sikker på at du vil fortsette?"</string>
<string name="invite_friends_text">"Hei, snakk med meg på %1$s: %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Rageshake for å rapportere feil"</string>
<string name="screen_bottom_sheet_manage_room_member_ban">"Fjern og utesteng medlem"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Utesteng"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"De vil ikke kunne bli med i dette rommet igjen hvis de blir invitert."</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Er du sikker på at du vil utestenge dette medlemmet?"</string>
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Utestenger %1$s"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Fjern"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"De vil kunne bli med i dette rommet igjen hvis de blir invitert."</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Er du sikker på at du vil fjerne dette medlemmet?"</string>
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Vis profil"</string>
<string name="screen_bottom_sheet_manage_room_member_remove">"Fjern fra rommet"</string>
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Fjerne medlem og utestenge fra å bli med i fremtiden?"</string>
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Fjerner %1$s…"</string>
<string name="screen_bottom_sheet_manage_room_member_unban">"Fjern utestengelsen fra rommet"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Opphev utestengelsen"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"De vil kunne bli med i rommet igjen hvis de blir invitert"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Er du sikker på at du vil oppheve utestengelsen av dette medlemmet?"</string>
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Oppheve utestengelsen av %1$s"</string>
<string name="screen_media_picker_error_failed_selection">"Kunne ikke velge medium, prøv igjen."</string>
<string name="screen_media_upload_preview_caption_warning">"Teksting er kanskje ikke synlig for personer som bruker eldre apper."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Kunne ikke behandle medier for opplasting, vennligst prøv igjen."</string>

View file

@ -174,6 +174,10 @@ Reden: %1$s."</string>
<string name="common_light">"Licht"</string>
<string name="common_link_copied_to_clipboard">"Link gekopieerd naar klembord"</string>
<string name="common_loading">"Laden…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d andere"</item>
<item quantity="other">"%d anderen"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d lid"</item>
<item quantity="other">"%1$d leden"</item>

View file

@ -200,6 +200,11 @@ Powód: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Link został skopiowany do schowka"</string>
<string name="common_loading">"Ładowanie…"</string>
<string name="common_loading_more">"Ładuję więcej…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d inny"</item>
<item quantity="few">"%d innych"</item>
<item quantity="many">"%d innych"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d członek"</item>
<item quantity="few">"%1$d członki"</item>

View file

@ -178,6 +178,10 @@
<string name="common_light">"Claro"</string>
<string name="common_link_copied_to_clipboard">"Link copiado para área de transferência"</string>
<string name="common_loading">"Carregando…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d outro"</item>
<item quantity="other">"%d outros"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d membro"</item>
<item quantity="other">"%1$d membros"</item>

View file

@ -189,6 +189,10 @@ Razão: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Ligação copiada para a área de transferência"</string>
<string name="common_loading">"A carregar…"</string>
<string name="common_loading_more">"A carregar mais…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d outro"</item>
<item quantity="other">"%d outros"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d membro"</item>
<item quantity="other">"%1$d membros"</item>

View file

@ -171,6 +171,11 @@ Motiv:%1$s."</string>
<string name="common_light">"Deschis"</string>
<string name="common_link_copied_to_clipboard">"Linkul a fost copiat în clipboard"</string>
<string name="common_loading">"Se încarcă…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d altul"</item>
<item quantity="few">"%d alții"</item>
<item quantity="other">"%d alții"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d membru"</item>
<item quantity="few">"%1$d membri"</item>

View file

@ -200,6 +200,11 @@
<string name="common_link_copied_to_clipboard">"Ссылка скопирована в буфер обмена"</string>
<string name="common_loading">"Загрузка…"</string>
<string name="common_loading_more">"Загрузить больше…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d"</item>
<item quantity="few">"%d другие"</item>
<item quantity="many">"%d другие"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d участник"</item>
<item quantity="few">"%1$d участников"</item>

View file

@ -200,6 +200,11 @@ Dôvod: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Odkaz bol skopírovaný do schránky"</string>
<string name="common_loading">"Načítava sa…"</string>
<string name="common_loading_more">"Načítava sa viac…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d ďalší"</item>
<item quantity="few">"%d ďalší"</item>
<item quantity="other">"%d ďalších"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d člen"</item>
<item quantity="few">"%1$d členovia"</item>
@ -343,6 +348,23 @@ Naozaj chcete pokračovať?"</string>
<string name="invite_friends_text">"Ahoj, porozprávajte sa so mnou na %1$s: %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Zúrivo potriasť pre nahlásenie chyby"</string>
<string name="screen_bottom_sheet_manage_room_member_ban">"Odstrániť a zakázať člena"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Zakázať"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Nebudú sa môcť pripojiť k tejto miestnosti znova ani ak budú pozvaní."</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Ste si istý, že chcete zakázať tohto člena?"</string>
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Zakazuje sa %1$s"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Odstrániť"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"V prípade pozvania sa budú môcť znova pripojiť k tejto miestnosti."</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Ste si istý, že chcete odstrániť tohto člena?"</string>
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Zobraziť profil"</string>
<string name="screen_bottom_sheet_manage_room_member_remove">"Odstrániť z miestnosti"</string>
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Odstrániť člena a zakázať vstup v budúcnosti?"</string>
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Odstraňuje sa %1$s…"</string>
<string name="screen_bottom_sheet_manage_room_member_unban">"Zrušiť zákaz prístupu do miestnosti"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Zrušiť zákaz"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"V prípade pozvania by sa mohli opäť pripojiť k miestnosti"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Naozaj chcete zrušiť zablokovanie tohto člena?"</string>
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Zrušenie zákazu pre %1$s"</string>
<string name="screen_media_picker_error_failed_selection">"Nepodarilo sa vybrať médium, skúste to prosím znova."</string>
<string name="screen_media_upload_preview_caption_warning">"Titulky nemusia byť viditeľné pre ľudí používajúcich staršie aplikácie."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>

View file

@ -198,6 +198,10 @@ Anledning:%1$s."</string>
<string name="common_link_copied_to_clipboard">"Länk kopierad till klippbordet"</string>
<string name="common_loading">"Laddar …"</string>
<string name="common_loading_more">"Laddar mer …"</string>
<plurals name="common_many_members">
<item quantity="one">"%d annan"</item>
<item quantity="other">"%d andra"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d medlem"</item>
<item quantity="other">"%1$d medlemmar"</item>

View file

@ -185,6 +185,10 @@ Neden: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Bağlantı panoya kopyalandı"</string>
<string name="common_loading">"Yükleniyor…"</string>
<string name="common_loading_more">"Daha fazla yükleniyor…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d diğer"</item>
<item quantity="other">"%d diğer"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d üye"</item>
<item quantity="other">"%1$d üye"</item>

View file

@ -200,6 +200,11 @@
<string name="common_link_copied_to_clipboard">"Посилання скопійовано в буфер обміну"</string>
<string name="common_loading">"Завантаження"</string>
<string name="common_loading_more">"Завантаження наступних…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d інший"</item>
<item quantity="few">"%d інші"</item>
<item quantity="many">"%d інші"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d учасник"</item>
<item quantity="few">"%1$d учасники"</item>

View file

@ -195,6 +195,9 @@
<string name="common_link_copied_to_clipboard">"連結已複製到剪貼簿"</string>
<string name="common_loading">"載入中…"</string>
<string name="common_loading_more">"載入更多……"</string>
<plurals name="common_many_members">
<item quantity="other">"其他 %d 個人"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="other">"%1$d 位成員"</item>
</plurals>

View file

@ -192,6 +192,9 @@
<string name="common_link_copied_to_clipboard">"链接已复制到剪贴板"</string>
<string name="common_loading">"正在加载…"</string>
<string name="common_loading_more">"正在加载更多……"</string>
<plurals name="common_many_members">
<item quantity="other">"其他 %d 人"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="other">"%1$d个成员"</item>
</plurals>

View file

@ -198,6 +198,10 @@ Reason: %1$s."</string>
<string name="common_link_copied_to_clipboard">"Link copied to clipboard"</string>
<string name="common_loading">"Loading…"</string>
<string name="common_loading_more">"Loading more…"</string>
<plurals name="common_many_members">
<item quantity="one">"%d other"</item>
<item quantity="other">"%d others"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d member"</item>
<item quantity="other">"%1$d members"</item>
@ -339,6 +343,23 @@ Are you sure you want to continue?"</string>
<string name="invite_friends_text">"Hey, talk to me on %1$s: %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Rageshake to report bug"</string>
<string name="screen_bottom_sheet_manage_room_member_ban">"Ban from room"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Ban"</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"They wont be able to join this room again if invited."</string>
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Are you sure you want to ban this member?"</string>
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Banning %1$s"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Remove"</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"They will be able to join this room again if invited."</string>
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Are you sure you want to remove this member?"</string>
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"View profile"</string>
<string name="screen_bottom_sheet_manage_room_member_remove">"Remove from room"</string>
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Remove member and ban from joining in the future?"</string>
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Removing %1$s…"</string>
<string name="screen_bottom_sheet_manage_room_member_unban">"Unban from room"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Unban"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"They would be able to join the room again if invited"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Are you sure you want to unban this member?"</string>
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Unbanning %1$s"</string>
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
<string name="screen_media_upload_preview_caption_warning">"Captions might not be visible to people using older apps."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>