Merge branch 'develop' into renovate/kotlin

This commit is contained in:
Benoit Marty 2024-10-21 08:44:21 +02:00 committed by GitHub
commit 71158bb0c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
363 changed files with 3030 additions and 2267 deletions

View file

@ -0,0 +1,61 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.androidutils.system
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.system.DateTimeObserver.Event
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import java.time.Instant
import javax.inject.Inject
interface DateTimeObserver {
val changes: Flow<Event>
sealed interface Event {
data object TimeZoneChanged : Event
data class DateChanged(val previous: Instant, val new: Instant) : Event
}
}
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultDateTimeObserver @Inject constructor(
@ApplicationContext context: Context
) : DateTimeObserver {
private val dateTimeReceiver = object : BroadcastReceiver() {
private var lastTime = Instant.now()
override fun onReceive(context: Context, intent: Intent) {
val newDate = Instant.now()
when (intent.action) {
Intent.ACTION_TIMEZONE_CHANGED -> changes.tryEmit(Event.TimeZoneChanged)
Intent.ACTION_DATE_CHANGED -> changes.tryEmit(Event.DateChanged(lastTime, newDate))
Intent.ACTION_TIME_CHANGED -> changes.tryEmit(Event.DateChanged(lastTime, newDate))
}
lastTime = newDate
}
}
override val changes = MutableSharedFlow<Event>(extraBufferCapacity = 10)
init {
context.registerReceiver(dateTimeReceiver, IntentFilter().apply {
addAction(Intent.ACTION_TIMEZONE_CHANGED)
addAction(Intent.ACTION_DATE_CHANGED)
addAction(Intent.ACTION_TIME_CHANGED)
})
}
}

View file

@ -26,7 +26,9 @@ sealed interface AsyncAction<out T> {
/**
* Represents an operation that is currently waiting for user confirmation.
*/
data object Confirming : AsyncAction<Nothing>
interface Confirming : AsyncAction<Nothing>
data object ConfirmingNoParams : Confirming
/**
* Represents an operation that is currently ongoing.
@ -70,7 +72,7 @@ sealed interface AsyncAction<out T> {
fun isUninitialized(): Boolean = this == Uninitialized
fun isConfirming(): Boolean = this == Confirming
fun isConfirming(): Boolean = this is Confirming
fun isLoading(): Boolean = this == Loading

View file

@ -7,6 +7,6 @@
package io.element.android.libraries.dateformatter.api
interface LastMessageTimestampFormatter {
fun interface LastMessageTimestampFormatter {
fun format(timestamp: Long?): String
}

View file

@ -11,7 +11,6 @@ import android.text.format.DateFormat
import android.text.format.DateUtils
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toJavaLocalDate
import kotlinx.datetime.toJavaLocalDateTime
@ -25,7 +24,7 @@ import kotlin.math.absoluteValue
class DateFormatters @Inject constructor(
private val locale: Locale,
private val clock: Clock,
private val timeZone: TimeZone,
private val timeZoneProvider: TimezoneProvider,
) {
private val onlyTimeFormatter: DateTimeFormatter by lazy {
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
@ -70,7 +69,7 @@ class DateFormatters @Inject constructor(
return if (period.years.absoluteValue >= 1) {
formatDateWithYear(dateToFormat)
} else if (useRelative && period.days.absoluteValue < 2 && period.months.absoluteValue < 1) {
getRelativeDay(dateToFormat.toInstant(timeZone).toEpochMilliseconds())
getRelativeDay(dateToFormat.toInstant(timeZoneProvider.provide()).toEpochMilliseconds())
} else {
formatDateWithMonth(dateToFormat)
}

View file

@ -10,21 +10,20 @@ package io.element.android.libraries.dateformatter.impl
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import javax.inject.Inject
class LocalDateTimeProvider @Inject constructor(
private val clock: Clock,
private val timezone: TimeZone,
private val timezoneProvider: TimezoneProvider,
) {
fun providesNow(): LocalDateTime {
val now: Instant = clock.now()
return now.toLocalDateTime(timezone)
return now.toLocalDateTime(timezoneProvider.provide())
}
fun providesFromTimestamp(timestamp: Long): LocalDateTime {
val tsInstant = Instant.fromEpochMilliseconds(timestamp)
return tsInstant.toLocalDateTime(timezone)
return tsInstant.toLocalDateTime(timezoneProvider.provide())
}
}

View file

@ -0,0 +1,14 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import kotlinx.datetime.TimeZone
fun interface TimezoneProvider {
fun provide(): TimeZone
}

View file

@ -10,6 +10,7 @@ package io.element.android.libraries.dateformatter.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.element.android.libraries.dateformatter.impl.TimezoneProvider
import io.element.android.libraries.di.AppScope
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
@ -25,5 +26,5 @@ object DateFormatterModule {
fun providesLocale(): Locale = Locale.getDefault()
@Provides
fun providesTimezone(): TimeZone = TimeZone.currentSystemDefault()
fun providesTimezone(): TimezoneProvider = TimezoneProvider { TimeZone.currentSystemDefault() }
}

View file

@ -93,7 +93,7 @@ class DefaultLastMessageTimestampFormatterTest {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1979-04-06T18:35:24.00Z"
val clock = FakeClock().apply { givenInstant(Instant.parse(now)) }
val dateFormatters = DateFormatters(Locale.US, clock, TimeZone.UTC)
val dateFormatters = DateFormatters(Locale.US, clock) { TimeZone.UTC }
assertThat(dateFormatters.formatDateWithFullFormat(Instant.parse(dat).toLocalDateTime(TimeZone.UTC))).isEqualTo("Friday, April 6, 1979")
}
@ -102,8 +102,8 @@ class DefaultLastMessageTimestampFormatterTest {
*/
private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): LastMessageTimestampFormatter {
val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) }
val localDateTimeProvider = LocalDateTimeProvider(clock, TimeZone.UTC)
val dateFormatters = DateFormatters(Locale.US, clock, TimeZone.UTC)
val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
val dateFormatters = DateFormatters(Locale.US, clock) { TimeZone.UTC }
return DefaultLastMessageTimestampFormatter(localDateTimeProvider, dateFormatters)
}
}

View file

@ -126,7 +126,7 @@ object BigIcon {
internal fun BigIconPreview() {
ElementPreview {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(10.dp)) {
val provider = BigIconStylePreviewProvider()
val provider = BigIconStyleProvider()
for (style in provider.values) {
BigIcon(style = style)
}
@ -134,7 +134,7 @@ internal fun BigIconPreview() {
}
}
internal class BigIconStylePreviewProvider : PreviewParameterProvider<BigIcon.Style> {
internal class BigIconStyleProvider : PreviewParameterProvider<BigIcon.Style> {
override val values: Sequence<BigIcon.Style>
get() = sequenceOf(
BigIcon.Style.Default(Icons.Filled.CatchingPokemon),

View file

@ -99,7 +99,7 @@ fun PageTitle(
@PreviewsDayNight
@Composable
internal fun PageTitleWithIconFullPreview(@PreviewParameter(BigIconStylePreviewProvider::class) style: BigIcon.Style) {
internal fun PageTitleWithIconFullPreview(@PreviewParameter(BigIconStyleProvider::class) style: BigIcon.Style) {
ElementPreview {
PageTitle(
modifier = Modifier.padding(top = 24.dp),

View file

@ -14,7 +14,7 @@ open class AsyncActionProvider : PreviewParameterProvider<AsyncAction<Unit>> {
override val values: Sequence<AsyncAction<Unit>>
get() = sequenceOf(
AsyncAction.Uninitialized,
AsyncAction.Confirming,
AsyncAction.ConfirmingNoParams,
AsyncAction.Loading,
AsyncAction.Failure(Exception("An error occurred")),
AsyncAction.Success(Unit),

View file

@ -34,7 +34,7 @@ fun <T> AsyncActionView(
async: AsyncAction<T>,
onSuccess: (T) -> Unit,
onErrorDismiss: () -> Unit,
confirmationDialog: @Composable () -> Unit = { },
confirmationDialog: @Composable (AsyncAction.Confirming) -> Unit = { },
progressDialog: @Composable () -> Unit = { AsyncActionViewDefaults.ProgressDialog() },
errorTitle: @Composable (Throwable) -> String = { ErrorDialogDefaults.title },
errorMessage: @Composable (Throwable) -> String = { it.message ?: it.toString() },
@ -42,7 +42,7 @@ fun <T> AsyncActionView(
) {
when (async) {
AsyncAction.Uninitialized -> Unit
AsyncAction.Confirming -> confirmationDialog()
is AsyncAction.Confirming -> confirmationDialog(async)
is AsyncAction.Loading -> progressDialog()
is AsyncAction.Failure -> {
if (onRetry == null) {

View file

@ -30,7 +30,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
internal class CompoundIconListPreviewProvider : PreviewParameterProvider<IconChunk> {
internal class CompoundIconChunkProvider : PreviewParameterProvider<IconChunk> {
override val values: Sequence<IconChunk>
get() {
val chunks = CompoundIcons.allResIds.chunked(36)
@ -41,7 +41,7 @@ internal class CompoundIconListPreviewProvider : PreviewParameterProvider<IconCh
}
}
internal class OtherIconListPreviewProvider : PreviewParameterProvider<IconChunk> {
internal class OtherIconChunkProvider : PreviewParameterProvider<IconChunk> {
override val values: Sequence<IconChunk>
get() {
val chunks = iconsOther.chunked(36)
@ -60,7 +60,7 @@ internal data class IconChunk(
@PreviewsDayNight
@Composable
internal fun IconsCompoundPreview(@PreviewParameter(CompoundIconListPreviewProvider::class) chunk: IconChunk) = ElementPreview {
internal fun IconsCompoundPreview(@PreviewParameter(CompoundIconChunkProvider::class) chunk: IconChunk) = ElementPreview {
IconsPreview(
title = "R.drawable.ic_compound_* ${chunk.index}/${chunk.total}",
iconsList = chunk.icons,
@ -73,7 +73,7 @@ internal fun IconsCompoundPreview(@PreviewParameter(CompoundIconListPreviewProvi
@PreviewsDayNight
@Composable
internal fun IconsOtherPreview(@PreviewParameter(OtherIconListPreviewProvider::class) iconChunk: IconChunk) = ElementPreview {
internal fun IconsOtherPreview(@PreviewParameter(OtherIconChunkProvider::class) iconChunk: IconChunk) = ElementPreview {
IconsPreview(
title = "R.drawable.ic_* ${iconChunk.index}/${iconChunk.total}",
iconsList = iconChunk.icons,

View file

@ -9,6 +9,6 @@ package io.element.android.libraries.eventformatter.api
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
interface RoomLastMessageFormatter {
fun interface RoomLastMessageFormatter {
fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence?
}

View file

@ -46,7 +46,8 @@ class DefaultPinnedMessagesBannerFormatter @Inject constructor(
return when (val content = event.content) {
is MessageContent -> processMessageContents(event, content)
is StickerContent -> {
content.body.prefixWith(CommonStrings.common_sticker)
val text = content.body ?: content.filename
text.prefixWith(CommonStrings.common_sticker)
}
is UnableToDecryptContent -> {
sp.getString(CommonStrings.common_waiting_for_decryption_key)
@ -76,25 +77,25 @@ class DefaultPinnedMessagesBannerFormatter @Inject constructor(
messageType.toPlainText(permalinkParser)
}
is VideoMessageType -> {
messageType.body.prefixWith(CommonStrings.common_video)
messageType.bestDescription.prefixWith(CommonStrings.common_video)
}
is ImageMessageType -> {
messageType.body.prefixWith(CommonStrings.common_image)
messageType.bestDescription.prefixWith(CommonStrings.common_image)
}
is StickerMessageType -> {
messageType.body.prefixWith(CommonStrings.common_sticker)
messageType.bestDescription.prefixWith(CommonStrings.common_sticker)
}
is LocationMessageType -> {
messageType.body.prefixWith(CommonStrings.common_shared_location)
}
is FileMessageType -> {
messageType.body.prefixWith(CommonStrings.common_file)
messageType.bestDescription.prefixWith(CommonStrings.common_file)
}
is AudioMessageType -> {
messageType.body.prefixWith(CommonStrings.common_audio)
messageType.bestDescription.prefixWith(CommonStrings.common_audio)
}
is VoiceMessageType -> {
messageType.body.prefixWith(CommonStrings.common_voice_message)
messageType.bestDescription.prefixWith(CommonStrings.common_voice_message)
}
is OtherMessageType -> {
messageType.body

View file

@ -67,7 +67,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
}
is StickerContent -> {
val message = sp.getString(CommonStrings.common_sticker) + " (" + content.body + ")"
val message = sp.getString(CommonStrings.common_sticker) + " (" + content.bestDescription + ")"
message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
}
is UnableToDecryptContent -> {

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="state_event_avatar_changed_too">"(изображение тоже было изменено)"</string>
<string name="state_event_avatar_url_changed">"%1$s сменили свое изображение"</string>
<string name="state_event_avatar_url_changed">"%1$s сменил своё изображение"</string>
<string name="state_event_avatar_url_changed_by_you">"Вы сменили изображение профиля"</string>
<string name="state_event_demoted_to_member">"%1$s был понижен в должности до участника"</string>
<string name="state_event_demoted_to_moderator">"%1$s был понижен в должности до модератора"</string>
<string name="state_event_demoted_to_member">"%1$s был понижен до участника"</string>
<string name="state_event_demoted_to_moderator">"%1$s был понижен до модератора"</string>
<string name="state_event_display_name_changed_from">"%1$s изменил свое отображаемое имя с %2$s на %3$s"</string>
<string name="state_event_display_name_changed_from_by_you">"Вы изменили свое отображаемое имя с %1$s на %2$s"</string>
<string name="state_event_display_name_removed">"%1$s удалил свое отображаемое имя (оно было %2$s)"</string>
@ -38,7 +38,7 @@
<string name="state_event_room_knock_retracted">"%1$s больше не заинтересован в присоединении"</string>
<string name="state_event_room_knock_retracted_by_you">"Вы отменили запрос на присоединение"</string>
<string name="state_event_room_leave">"%1$s покинул комнату"</string>
<string name="state_event_room_leave_by_you">"Вы вышли из комнаты"</string>
<string name="state_event_room_leave_by_you">"Вы покинули комнату"</string>
<string name="state_event_room_name_changed">"%1$s изменил название комнаты на: %2$s"</string>
<string name="state_event_room_name_changed_by_you">"Вы изменили название комнаты на: %1$s"</string>
<string name="state_event_room_name_removed">"%1$s удалил название комнаты"</string>

View file

@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
@ -46,6 +45,7 @@ import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.timeline.aPollContent
import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent
import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails
import io.element.android.libraries.matrix.test.timeline.aStickerContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
@ -91,7 +91,7 @@ class DefaultPinnedMessagesBannerFormatterTest {
fun `Sticker content`() {
val body = "a sticker body"
val info = ImageInfo(null, null, null, null, null, null, null)
val message = createRoomEvent(false, null, StickerContent(body, info, aMediaSource(url = "url")))
val message = createRoomEvent(false, null, aStickerContent(body, info, aMediaSource(url = "url")))
val result = formatter.format(message)
val expectedBody = "Sticker: a sticker body"
assertThat(result.toString()).isEqualTo(expectedBody)
@ -135,11 +135,11 @@ class DefaultPinnedMessagesBannerFormatterTest {
val sharedContentMessagesTypes = arrayOf(
TextMessageType(body, null),
VideoMessageType(body, null, null, MediaSource("url"), null),
AudioMessageType(body, MediaSource("url"), null),
VoiceMessageType(body, MediaSource("url"), null, null),
AudioMessageType(body, null, null, MediaSource("url"), null),
VoiceMessageType(body, null, null, MediaSource("url"), null, null),
ImageMessageType(body, null, null, MediaSource("url"), null),
StickerMessageType(body, MediaSource("url"), null),
FileMessageType(body, MediaSource("url"), null),
StickerMessageType(body, null, null, MediaSource("url"), null),
FileMessageType(body, null, null, MediaSource("url"), null),
LocationMessageType(body, "geo:1,2", null),
NoticeMessageType(body, null),
EmoteMessageType(body, null),

View file

@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
@ -46,6 +45,7 @@ import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.timeline.aPollContent
import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent
import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails
import io.element.android.libraries.matrix.test.timeline.aStickerContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import org.junit.Before
@ -98,7 +98,7 @@ class DefaultRoomLastMessageFormatterTest {
fun `Sticker content`() {
val body = "a sticker body"
val info = ImageInfo(null, null, null, null, null, null, null)
val message = createRoomEvent(false, null, StickerContent(body, info, aMediaSource(url = "url")))
val message = createRoomEvent(false, null, aStickerContent(body, info, aMediaSource(url = "url")))
val result = formatter.format(message, false)
val expectedBody = someoneElseId.toString() + ": Sticker (a sticker body)"
assertThat(result.toString()).isEqualTo(expectedBody)
@ -179,11 +179,11 @@ class DefaultRoomLastMessageFormatterTest {
val sharedContentMessagesTypes = arrayOf(
TextMessageType(body, null),
VideoMessageType(body, null, null, MediaSource("url"), null),
AudioMessageType(body, MediaSource("url"), null),
VoiceMessageType(body, MediaSource("url"), null, null),
AudioMessageType(body, null, null, MediaSource("url"), null),
VoiceMessageType(body, null, null, MediaSource("url"), null, null),
ImageMessageType(body, null, null, MediaSource("url"), null),
StickerMessageType(body, MediaSource("url"), null),
FileMessageType(body, MediaSource("url"), null),
StickerMessageType(body, null, null, MediaSource("url"), null),
FileMessageType(body, null, null, MediaSource("url"), null),
LocationMessageType(body, "geo:1,2", null),
NoticeMessageType(body, null),
EmoteMessageType(body, null),

View file

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

View file

@ -0,0 +1,51 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
sealed interface OidcPrompt {
/**
* The Authorization Server must not display any authentication or consent
* user interface pages.
*/
data object None : OidcPrompt
/**
* The Authorization Server should prompt the End-User for
* reauthentication.
*/
data object Login : OidcPrompt
/**
* The Authorization Server should prompt the End-User for consent before
* returning information to the Client.
*/
data object Consent : OidcPrompt
/**
* The Authorization Server should prompt the End-User to select a user
* account.
*
* This enables an End-User who has multiple accounts at the Authorization
* Server to select amongst the multiple accounts that they might have
* current sessions for.
*/
data object SelectAccount : OidcPrompt
/**
* The Authorization Server should prompt the End-User to create a user
* account.
*
* Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
*/
data object Create : OidcPrompt
/**
* An unknown value.
*/
data class Unknown(val value: String) : OidcPrompt
}

View file

@ -24,4 +24,9 @@ value class UserId(val value: String) : Serializable {
}
override fun toString(): String = value
val extractedDisplayName: String
get() = value
.removePrefix("@")
.substringBefore(":")
}

View file

@ -25,14 +25,14 @@ interface MatrixMediaLoader {
/**
* @param source to fetch the data for.
* @param mimeType: optional mime type.
* @param body: optional body which will be used to name the file.
* @param filename: optional String which will be used to name the file.
* @param useCache: if true, the rust sdk will cache the media in its store.
* @return a [Result] of [MediaFile]
*/
suspend fun downloadMediaFile(
source: MediaSource,
mimeType: String?,
body: String?,
filename: String?,
useCache: Boolean = true,
): Result<MediaFile>
}

View file

@ -10,5 +10,6 @@ package io.element.android.libraries.matrix.api.room
enum class CurrentUserMembership {
INVITED,
JOINED,
LEFT
LEFT,
KNOCKED,
}

View file

@ -14,7 +14,6 @@ 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.SessionId
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.encryption.identity.IdentityStateChange
import io.element.android.libraries.matrix.api.media.AudioInfo
@ -29,6 +28,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerL
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.timeline.ReceiptType
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
@ -150,7 +150,7 @@ interface MatrixRoom : Closeable {
suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler>
suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit>
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit>
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>

View file

@ -51,6 +51,12 @@ data class RoomMember(
isNameAmbiguous -> "$displayName ($userId)"
else -> displayName
}
val displayNameOrDefault: String
get() = when {
displayName == null -> userId.extractedDisplayName
else -> displayName
}
}
enum class RoomMembershipState {

View file

@ -11,7 +11,6 @@ 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.core.UniqueId
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
@ -20,7 +19,9 @@ 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.IntentionalMention
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.io.File
@ -57,8 +58,7 @@ interface Timeline : AutoCloseable {
): Result<Unit>
suspend fun editMessage(
originalEventId: EventId?,
transactionId: TransactionId?,
eventOrTransactionId: EventOrTransactionId,
body: String, htmlBody: String?,
intentionalMentions: List<IntentionalMention>,
): Result<Unit>
@ -89,17 +89,18 @@ interface Timeline : AutoCloseable {
progressCallback: ProgressCallback?
): Result<MediaUploadHandler>
suspend fun redactEvent(eventId: EventId?, transactionId: TransactionId?, reason: String?): Result<Unit>
suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result<Unit>
suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler>
suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler>
suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): 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 cancelSend(transactionId: TransactionId): Result<Unit> =
redactEvent(transactionId.toEventOrTransactionId(), reason = null)
/**
* Share a location message in the room.

View file

@ -30,10 +30,14 @@ data class MessageContent(
data object RedactedContent : EventContent
data class StickerContent(
val body: String,
val filename: String,
val body: String?,
val info: ImageInfo,
val source: MediaSource,
) : EventContent
) : EventContent {
val bestDescription: String
get() = body ?: filename
}
data class PollContent(
val question: String,

View file

@ -0,0 +1,37 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.timeline.item.event
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
@Immutable
sealed interface EventOrTransactionId {
@JvmInline
value class Event(val id: EventId) : EventOrTransactionId
@JvmInline
value class Transaction(val id: TransactionId) : EventOrTransactionId
val eventId: EventId?
get() = (this as? Event)?.id
companion object {
fun from(eventId: EventId?, transactionId: TransactionId?): EventOrTransactionId {
return when {
eventId != null -> Event(eventId)
transactionId != null -> Transaction(transactionId)
else -> throw IllegalArgumentException("EventId and TransactionId are both null")
}
}
}
}
fun EventId.toEventOrTransactionId() = EventOrTransactionId.Event(this)
fun TransactionId.toEventOrTransactionId() = EventOrTransactionId.Transaction(this)

View file

@ -18,7 +18,6 @@ data class EventTimelineItem(
val transactionId: TransactionId?,
val isEditable: Boolean,
val canBeRepliedTo: Boolean,
val isLocal: Boolean,
val isOwn: Boolean,
val isRemote: Boolean,
val localSendState: LocalEventSendState?,
@ -28,9 +27,9 @@ data class EventTimelineItem(
val senderProfile: ProfileTimelineDetails,
val timestamp: Long,
val content: EventContent,
val debugInfoProvider: EventDebugInfoProvider,
val origin: TimelineItemEventOrigin?,
val messageShieldProvider: EventShieldsProvider,
val timelineItemDebugInfoProvider: TimelineItemDebugInfoProvider,
val messageShieldProvider: MessageShieldProvider,
) {
fun inReplyTo(): InReplyTo? {
return (content as? MessageContent)?.inReplyTo
@ -46,10 +45,10 @@ data class EventTimelineItem(
}
}
fun interface EventDebugInfoProvider {
fun get(): TimelineItemDebugInfo
fun interface TimelineItemDebugInfoProvider {
operator fun invoke(): TimelineItemDebugInfo
}
fun interface EventShieldsProvider {
fun getShield(strict: Boolean): MessageShield?
fun interface MessageShieldProvider {
operator fun invoke(strict: Boolean): MessageShield?
}

View file

@ -18,24 +18,37 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
@Immutable
sealed interface MessageType
@Immutable
sealed interface MessageTypeWithAttachment : MessageType {
val filename: String
val caption: String?
val formattedCaption: FormattedBody?
val bestDescription: String
get() = caption ?: filename
}
data class EmoteMessageType(
val body: String,
val formatted: FormattedBody?
) : MessageType
data class ImageMessageType(
val body: String,
val formatted: FormattedBody?,
val filename: String?,
override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val source: MediaSource,
val info: ImageInfo?
) : MessageType
) : MessageTypeWithAttachment
// FIXME This is never used in production code.
data class StickerMessageType(
val body: String,
override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val source: MediaSource,
val info: ImageInfo?
) : MessageType
) : MessageTypeWithAttachment
data class LocationMessageType(
val body: String,
@ -44,31 +57,37 @@ data class LocationMessageType(
) : MessageType
data class AudioMessageType(
val body: String,
override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val source: MediaSource,
val info: AudioInfo?,
) : MessageType
) : MessageTypeWithAttachment
data class VoiceMessageType(
val body: String,
override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val source: MediaSource,
val info: AudioInfo?,
val details: AudioDetails?,
) : MessageType
) : MessageTypeWithAttachment
data class VideoMessageType(
val body: String,
val formatted: FormattedBody?,
val filename: String?,
override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val source: MediaSource,
val info: VideoInfo?
) : MessageType
) : MessageTypeWithAttachment
data class FileMessageType(
val body: String,
override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val source: MediaSource,
val info: FileInfo?
) : MessageType
) : MessageTypeWithAttachment
data class NoticeMessageType(
val body: String,

View file

@ -0,0 +1,22 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.OidcPrompt
import org.matrix.rustcomponents.sdk.OidcPrompt as RustOidcPrompt
internal fun OidcPrompt.toRustPrompt(): RustOidcPrompt {
return when (this) {
OidcPrompt.None -> RustOidcPrompt.None
OidcPrompt.Login -> RustOidcPrompt.Login
OidcPrompt.Consent -> RustOidcPrompt.Consent
OidcPrompt.SelectAccount -> RustOidcPrompt.SelectAccount
OidcPrompt.Create -> RustOidcPrompt.Create
is OidcPrompt.Unknown -> RustOidcPrompt.Unknown(value)
}
}

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.auth.OidcPrompt
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
@ -181,11 +182,14 @@ class RustMatrixAuthenticationService @Inject constructor(
private var pendingOidcAuthorizationData: OidcAuthorizationData? = null
override suspend fun getOidcUrl(): Result<OidcDetails> {
override suspend fun getOidcUrl(prompt: OidcPrompt): Result<OidcDetails> {
return withContext(coroutineDispatchers.io) {
runCatching {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val oidcAuthenticationData = client.urlForOidcLogin(oidcConfigurationProvider.get())
val oidcAuthenticationData = client.urlForOidc(
oidcConfiguration = oidcConfigurationProvider.get(),
prompt = prompt.toRustPrompt(),
)
val url = oidcAuthenticationData.loginUrl()
pendingOidcAuthorizationData = oidcAuthenticationData
OidcDetails(url)

View file

@ -63,7 +63,7 @@ class RustMediaLoader(
override suspend fun downloadMediaFile(
source: MediaSource,
mimeType: String?,
body: String?,
filename: String?,
useCache: Boolean,
): Result<MediaFile> =
withContext(mediaDispatcher) {
@ -71,7 +71,7 @@ class RustMediaLoader(
source.toRustMediaSource().use { mediaSource ->
val mediaFile = innerClient.getMediaFile(
mediaSource = mediaSource,
body = body,
filename = filename,
mimeType = mimeType?.takeIf { MimeTypes.hasSubtype(it) } ?: MimeTypes.OctetStream,
useCache = useCache,
tempDir = cacheDirectory.path,

View file

@ -19,6 +19,7 @@ import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.RoomHero
import org.matrix.rustcomponents.sdk.Membership as RustMembership
import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo
@ -65,6 +66,7 @@ fun RustMembership.map(): CurrentUserMembership = when (this) {
RustMembership.INVITED -> CurrentUserMembership.INVITED
RustMembership.JOINED -> CurrentUserMembership.JOINED
RustMembership.LEFT -> CurrentUserMembership.LEFT
Membership.KNOCKED -> CurrentUserMembership.KNOCKED
}
fun RustRoomNotificationMode.map(): RoomNotificationMode = when (this) {

View file

@ -17,7 +17,6 @@ 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.SessionId
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.encryption.identity.IdentityStateChange
import io.element.android.libraries.matrix.api.media.AudioInfo
@ -42,6 +41,7 @@ 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.timeline.ReceiptType
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.mapper.map
@ -471,8 +471,8 @@ class RustMatrixRoom(
return liveTimeline.sendFile(file, fileInfo, progressCallback)
}
override suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit> {
return liveTimeline.toggleReaction(emoji, uniqueId)
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> {

View file

@ -0,0 +1,16 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import org.matrix.rustcomponents.sdk.EventOrTransactionId as RustEventOrTransactionId
fun EventOrTransactionId.toRustEventOrTransactionId() = when (this) {
is EventOrTransactionId.Event -> RustEventOrTransactionId.EventId(id.value)
is EventOrTransactionId.Transaction -> RustEventOrTransactionId.TransactionId(id.value)
}

View file

@ -23,7 +23,7 @@ class MatrixTimelineItemMapper(
private val eventTimelineItemMapper: EventTimelineItemMapper,
) {
fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use {
val uniqueId = UniqueId(timelineItem.uniqueId())
val uniqueId = UniqueId(timelineItem.uniqueId().id)
val asEvent = it.asEvent()
if (asEvent != null) {
val eventTimelineItem = eventTimelineItemMapper.map(asEvent)

View file

@ -10,8 +10,6 @@ package io.element.android.libraries.matrix.impl.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.core.UniqueId
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 +24,7 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
@ -65,8 +64,6 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.EditedContent
import org.matrix.rustcomponents.sdk.EventOrTransactionId
import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.FormattedBody
import org.matrix.rustcomponents.sdk.MessageFormat
import org.matrix.rustcomponents.sdk.PollData
@ -75,6 +72,7 @@ import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import uniffi.matrix_sdk_ui.LiveBackPaginationStatus
import java.io.File
import org.matrix.rustcomponents.sdk.EventOrTransactionId as RustEventOrTransactionId
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
private const val PAGINATION_SIZE = 50
@ -280,31 +278,23 @@ class RustTimeline(
}
}
override suspend fun redactEvent(eventId: EventId?, transactionId: TransactionId?, reason: String?): Result<Unit> = withContext(dispatcher) {
override suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result<Unit> = withContext(dispatcher) {
runCatching {
val eventOrTransactionId = if (eventId != null) {
EventOrTransactionId.EventId(eventId.value)
} else {
EventOrTransactionId.TransactionId(transactionId!!.value)
}
inner.redactEvent(eventOrTransactionId = eventOrTransactionId, reason = reason)
inner.redactEvent(
eventOrTransactionId = eventOrTransactionId.toRustEventOrTransactionId(),
reason = reason,
)
}
}
override suspend fun editMessage(
originalEventId: EventId?,
transactionId: TransactionId?,
eventOrTransactionId: EventOrTransactionId,
body: String,
htmlBody: String?,
intentionalMentions: List<IntentionalMention>,
): Result<Unit> =
withContext(dispatcher) {
runCatching<Unit> {
val eventOrTransactionId = if (originalEventId != null) {
EventOrTransactionId.EventId(originalEventId.value)
} else {
EventOrTransactionId.TransactionId(transactionId!!.value)
}
val editedContent = EditedContent.RoomMessage(
content = MessageEventContent.from(
body = body,
@ -314,7 +304,7 @@ class RustTimeline(
)
inner.edit(
newContent = editedContent,
eventOrTransactionId = eventOrTransactionId,
eventOrTransactionId = eventOrTransactionId.toRustEventOrTransactionId(),
)
}
}
@ -354,21 +344,6 @@ class RustTimeline(
}
}
@Throws
@Suppress("UnusedPrivateMember")
private suspend fun getEventTimelineItem(eventId: EventId?, transactionId: TransactionId?): EventTimelineItem {
return try {
when {
eventId != null -> inner.getEventTimelineItemByEventId(eventId.value)
transactionId != null -> inner.getEventTimelineItemByTransactionId(transactionId.value)
else -> error("Either eventId or transactionId must be non-null")
}
} catch (e: Exception) {
Timber.e(e, "Failed to get event timeline item")
throw TimelineException.EventNotFound
}
}
override suspend fun sendVideo(
file: File,
thumbnailFile: File?,
@ -410,9 +385,12 @@ class RustTimeline(
}
}
override suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit> = withContext(dispatcher) {
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = withContext(dispatcher) {
runCatching {
inner.toggleReaction(key = emoji, uniqueId = uniqueId.value)
inner.toggleReaction(
key = emoji,
itemId = eventOrTransactionId.toRustEventOrTransactionId(),
)
}
}
@ -424,9 +402,6 @@ class RustTimeline(
}
}
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> =
redactEvent(eventId = null, transactionId = transactionId, reason = null)
override suspend fun sendLocation(
body: String,
geoUri: String,
@ -479,7 +454,7 @@ class RustTimeline(
)
inner.edit(
newContent = editedContent,
eventOrTransactionId = EventOrTransactionId.EventId(pollStartId.value),
eventOrTransactionId = RustEventOrTransactionId.EventId(pollStartId.value),
)
}.map { }
}

View file

@ -50,14 +50,18 @@ class EventMessageMapper {
when (type.content.voice) {
null -> {
AudioMessageType(
body = type.content.body,
filename = type.content.filename,
caption = type.content.caption,
formattedCaption = type.content.formattedCaption?.map(),
source = type.content.source.map(),
info = type.content.info?.map(),
)
}
else -> {
VoiceMessageType(
body = type.content.body,
filename = type.content.filename,
caption = type.content.caption,
formattedCaption = type.content.formattedCaption?.map(),
source = type.content.source.map(),
info = type.content.info?.map(),
details = type.content.audio?.map(),
@ -66,10 +70,22 @@ class EventMessageMapper {
}
}
is RustMessageType.File -> {
FileMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
FileMessageType(
filename = type.content.filename,
caption = type.content.caption,
formattedCaption = type.content.formattedCaption?.map(),
source = type.content.source.map(),
info = type.content.info?.map(),
)
}
is RustMessageType.Image -> {
ImageMessageType(type.content.body, type.content.formatted?.map(), type.content.filename, type.content.source.map(), type.content.info?.map())
ImageMessageType(
filename = type.content.filename,
caption = type.content.caption,
formattedCaption = type.content.formattedCaption?.map(),
source = type.content.source.map(),
info = type.content.info?.map(),
)
}
is RustMessageType.Notice -> {
NoticeMessageType(type.content.body, type.content.formatted?.map())
@ -81,7 +97,13 @@ class EventMessageMapper {
EmoteMessageType(type.content.body, type.content.formatted?.map())
}
is RustMessageType.Video -> {
VideoMessageType(type.content.body, type.content.formatted?.map(), type.content.filename, type.content.source.map(), type.content.info?.map())
VideoMessageType(
filename = type.content.filename,
caption = type.content.caption,
formattedCaption = type.content.formattedCaption?.map(),
source = type.content.source.map(),
info = type.content.info?.map(),
)
}
is RustMessageType.Location -> {
LocationMessageType(type.content.body, type.content.geoUri, type.content.description)

View file

@ -12,9 +12,7 @@ import io.element.android.libraries.matrix.api.core.EventId
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.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.EventDebugInfoProvider
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
import io.element.android.libraries.matrix.api.timeline.item.event.EventShieldsProvider
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
@ -27,12 +25,10 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.EventOrTransactionId
import org.matrix.rustcomponents.sdk.EventSendState
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfoProvider
import org.matrix.rustcomponents.sdk.Reaction
import org.matrix.rustcomponents.sdk.ShieldState
import uniffi.matrix_sdk_common.ShieldStateCode
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
import org.matrix.rustcomponents.sdk.EventShieldsProvider as RustEventShieldsProvider
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo
import org.matrix.rustcomponents.sdk.ProfileDetails as RustProfileDetails
@ -48,7 +44,6 @@ class EventTimelineItemMapper(
transactionId = eventOrTransactionId.transactionId(),
isEditable = isEditable,
canBeRepliedTo = canBeRepliedTo,
isLocal = isLocal,
isOwn = isOwn,
isRemote = isRemote,
localSendState = localSendState?.map(),
@ -58,9 +53,9 @@ class EventTimelineItemMapper(
senderProfile = senderProfile.map(),
timestamp = timestamp.toLong(),
content = contentMapper.map(content),
debugInfoProvider = RustEventDebugInfoProvider(debugInfoProvider),
origin = origin?.map(),
messageShieldProvider = RustEventShieldsProvider(shieldsProvider)
timelineItemDebugInfoProvider = { lazyProvider.debugInfo().map() },
messageShieldProvider = { strict -> lazyProvider.getShields(strict)?.map() },
)
}
}
@ -168,18 +163,6 @@ private fun ShieldState?.map(): MessageShield? {
}
}
class RustEventDebugInfoProvider(private val debugInfoProvider: EventTimelineItemDebugInfoProvider) : EventDebugInfoProvider {
override fun get(): TimelineItemDebugInfo {
return debugInfoProvider.get().map()
}
}
class RustEventShieldsProvider(private val shieldsProvider: RustEventShieldsProvider) : EventShieldsProvider {
override fun getShield(strict: Boolean): MessageShield? {
return shieldsProvider.getShields(strict)?.map()
}
}
private fun EventOrTransactionId.eventId(): EventId? {
return (this as? EventOrTransactionId.EventId)?.let { EventId(it.eventId) }
}

View file

@ -84,7 +84,8 @@ class TimelineEventContentMapper(
}
is TimelineItemContent.Sticker -> {
StickerContent(
body = it.body,
filename = it.body,
body = null,
info = it.info.map(),
source = it.source.map(),
)

View file

@ -7,8 +7,7 @@
package io.element.android.libraries.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustEventShieldsProvider
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustEventTimelineItemDebugInfoProvider
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustLazyTimelineItemProvider
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import org.matrix.rustcomponents.sdk.EventOrTransactionId
@ -23,7 +22,6 @@ import org.matrix.rustcomponents.sdk.TimelineItemContent
import uniffi.matrix_sdk_ui.EventItemOrigin
fun aRustEventTimelineItem(
isLocal: Boolean = false,
isRemote: Boolean = true,
eventOrTransactionId: EventOrTransactionId = EventOrTransactionId.EventId(AN_EVENT_ID.value),
sender: String = A_USER_ID.value,
@ -40,7 +38,6 @@ fun aRustEventTimelineItem(
canBeRepliedTo: Boolean = true,
shieldsState: ShieldState? = null,
) = EventTimelineItem(
isLocal = isLocal,
isRemote = isRemote,
eventOrTransactionId = eventOrTransactionId,
sender = sender,
@ -50,10 +47,12 @@ fun aRustEventTimelineItem(
isEditable = isEditable,
canBeRepliedTo = canBeRepliedTo,
content = content,
debugInfoProvider = FakeRustEventTimelineItemDebugInfoProvider(debugInfo),
shieldsProvider = FakeRustEventShieldsProvider(shieldsState),
localSendState = localSendState,
reactions = reactions,
readReceipts = readReceipts,
origin = origin,
lazyProvider = FakeRustLazyTimelineItemProvider(
debugInfo = debugInfo,
shieldsState = shieldsState,
)
)

View file

@ -1,18 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.fakes
import org.matrix.rustcomponents.sdk.EventShieldsProvider
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.ShieldState
class FakeRustEventShieldsProvider(
private val shieldsState: ShieldState? = null,
) : EventShieldsProvider(NoPointer) {
override fun getShields(strict: Boolean): ShieldState? = shieldsState
}

View file

@ -9,11 +9,14 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes
import io.element.android.libraries.matrix.impl.fixtures.factories.anEventTimelineItemDebugInfo
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfoProvider
import org.matrix.rustcomponents.sdk.LazyTimelineItemProvider
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.ShieldState
class FakeRustEventTimelineItemDebugInfoProvider(
class FakeRustLazyTimelineItemProvider(
private val debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(),
) : EventTimelineItemDebugInfoProvider(NoPointer) {
override fun get(): EventTimelineItemDebugInfo = debugInfo
private val shieldsState: ShieldState? = null,
) : LazyTimelineItemProvider(NoPointer) {
override fun getShields(strict: Boolean) = shieldsState
override fun debugInfo() = debugInfo
}

View file

@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes
import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.TimelineItem
import org.matrix.rustcomponents.sdk.TimelineUniqueId
import org.matrix.rustcomponents.sdk.VirtualTimelineItem
class FakeRustTimelineItem(
@ -18,5 +19,5 @@ class FakeRustTimelineItem(
override fun asEvent(): EventTimelineItem? = asEventResult
override fun asVirtual(): VirtualTimelineItem? = null
override fun fmtDebug(): String = "fmtDebug"
override fun uniqueId(): String = "uniqueId"
override fun uniqueId(): TimelineUniqueId = TimelineUniqueId("uniqueId")
}

View file

@ -11,6 +11,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.auth.OidcPrompt
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
@ -80,7 +81,7 @@ class FakeMatrixAuthenticationService(
return importCreatedSessionLambda(externalSession)
}
override suspend fun getOidcUrl(): Result<OidcDetails> = simulateLongTask {
override suspend fun getOidcUrl(prompt: OidcPrompt): Result<OidcDetails> = simulateLongTask {
oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA)
}

View file

@ -35,7 +35,7 @@ class FakeMatrixMediaLoader : MatrixMediaLoader {
override suspend fun downloadMediaFile(
source: MediaSource,
mimeType: String?,
body: String?,
filename: String?,
useCache: Boolean,
): Result<MediaFile> = simulateLongTask {
if (shouldFail) {

View file

@ -13,14 +13,15 @@ import io.element.android.libraries.matrix.api.room.InvitedRoom
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
class FakeInvitedRoom(
override val sessionId: SessionId = A_SESSION_ID,
override val roomId: RoomId = A_ROOM_ID,
private val declineInviteResult: () -> Result<Unit> = { lambdaError() }
) : InvitedRoom {
override suspend fun declineInvite(): Result<Unit> {
return declineInviteResult()
override suspend fun declineInvite(): Result<Unit> = simulateLongTask {
declineInviteResult()
}
override fun close() = Unit

View file

@ -14,7 +14,6 @@ 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.SessionId
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.encryption.identity.IdentityStateChange
import io.element.android.libraries.matrix.api.media.AudioInfo
@ -38,6 +37,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerL
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.timeline.ReceiptType
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.A_ROOM_ID
@ -95,7 +95,7 @@ class FakeMatrixRoom(
private val editMessageLambda: (EventId, String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _, _ -> lambdaError() },
private val sendMessageResult: (String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _ -> lambdaError() },
private val updateUserRoleResult: () -> Result<Unit> = { lambdaError() },
private val toggleReactionResult: (String, UniqueId) -> Result<Unit> = { _, _ -> lambdaError() },
private val toggleReactionResult: (String, EventOrTransactionId) -> Result<Unit> = { _, _ -> lambdaError() },
private val retrySendMessageResult: (TransactionId) -> Result<Unit> = { lambdaError() },
private val cancelSendResult: (TransactionId) -> Result<Unit> = { lambdaError() },
private val forwardEventResult: (EventId, List<RoomId>) -> Result<Unit> = { _, _ -> lambdaError() },
@ -236,8 +236,8 @@ class FakeMatrixRoom(
sendMessageResult(body, htmlBody, intentionalMentions)
}
override suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit> {
return toggleReactionResult(emoji, uniqueId)
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> {
return toggleReactionResult(emoji, eventOrTransactionId)
}
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> = simulateLongTask {

View file

@ -10,8 +10,6 @@ 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.core.UniqueId
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
@ -23,6 +21,7 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType
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.timeline.item.event.InReplyTo
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.tests.testutils.lambda.lambdaError
@ -63,35 +62,31 @@ class FakeTimeline(
intentionalMentions: List<IntentionalMention>,
): Result<Unit> = sendMessageLambda(body, htmlBody, intentionalMentions)
var redactEventLambda: (eventId: EventId?, transactionId: TransactionId?, reason: String?) -> Result<Unit> = { _, _, _ ->
var redactEventLambda: (eventOrTransactionId: EventOrTransactionId, reason: String?) -> Result<Unit> = { _, _ ->
Result.success(Unit)
}
override suspend fun redactEvent(
eventId: EventId?,
transactionId: TransactionId?,
eventOrTransactionId: EventOrTransactionId,
reason: String?
): Result<Unit> = redactEventLambda(eventId, transactionId, reason)
): Result<Unit> = redactEventLambda(eventOrTransactionId, reason)
var editMessageLambda: (
originalEventId: EventId?,
transactionId: TransactionId?,
eventOrTransactionId: EventOrTransactionId,
body: String,
htmlBody: String?,
intentionalMentions: List<IntentionalMention>,
) -> Result<Unit> = { _, _, _, _, _ ->
) -> Result<Unit> = { _, _, _, _ ->
Result.success(Unit)
}
override suspend fun editMessage(
originalEventId: EventId?,
transactionId: TransactionId?,
eventOrTransactionId: EventOrTransactionId,
body: String,
htmlBody: String?,
intentionalMentions: List<IntentionalMention>,
): Result<Unit> = editMessageLambda(
originalEventId,
transactionId,
eventOrTransactionId,
body,
htmlBody,
intentionalMentions
@ -211,14 +206,15 @@ class FakeTimeline(
progressCallback
)
var toggleReactionLambda: (emoji: String, uniqueId: UniqueId) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit> = toggleReactionLambda(emoji, uniqueId)
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = toggleReactionLambda(
emoji,
eventOrTransactionId
)
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = forwardEventLambda(eventId, roomIds)
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = redactEvent(null, transactionId, null)
var sendLocationLambda: (
body: String,
geoUri: String,

View file

@ -10,6 +10,8 @@ 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.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
@ -25,6 +27,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID
@ -39,7 +42,6 @@ fun anEventTimelineItem(
transactionId: TransactionId? = null,
isEditable: Boolean = false,
canBeRepliedTo: Boolean = false,
isLocal: Boolean = false,
isOwn: Boolean = false,
isRemote: Boolean = false,
localSendState: LocalEventSendState? = null,
@ -56,7 +58,6 @@ fun anEventTimelineItem(
transactionId = transactionId,
isEditable = isEditable,
canBeRepliedTo = canBeRepliedTo,
isLocal = isLocal,
isOwn = isOwn,
isRemote = isRemote,
localSendState = localSendState,
@ -66,8 +67,8 @@ fun anEventTimelineItem(
senderProfile = senderProfile,
timestamp = timestamp,
content = content,
debugInfoProvider = { debugInfo },
origin = null,
timelineItemDebugInfoProvider = { debugInfo },
messageShieldProvider = { messageShield },
)
@ -110,6 +111,18 @@ fun aMessageContent(
type = messageType
)
fun aStickerContent(
filename: String = "filename",
info: ImageInfo,
mediaSource: MediaSource,
body: String? = null,
) = StickerContent(
filename = filename,
body = body,
info = info,
source = mediaSource,
)
fun aTimelineItemDebugInfo(
model: String = "Rust(Model())",
originalJson: String? = null,

View file

@ -44,7 +44,7 @@ internal class CoilMediaFetcher(
*
*/
private suspend fun fetchFile(mediaSource: MediaSource, kind: MediaRequestData.Kind.File): FetchResult? {
return mediaLoader.downloadMediaFile(mediaSource, kind.mimeType, kind.body)
return mediaLoader.downloadMediaFile(mediaSource, kind.mimeType, kind.fileName)
.map { mediaFile ->
val file = mediaFile.toFile()
SourceResult(

View file

@ -26,7 +26,12 @@ data class MediaRequestData(
) {
sealed interface Kind {
data object Content : Kind
data class File(val body: String?, val mimeType: String) : Kind
data class File(
val fileName: String,
val mimeType: String,
) : Kind
data class Thumbnail(val width: Long, val height: Long) : Kind {
constructor(size: Long) : this(size, size)
}

View file

@ -49,11 +49,11 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
),
aMessageContent(
body = "Audio",
type = AudioMessageType("Audio", MediaSource("url"), null),
type = AudioMessageType("Audio", null, null, MediaSource("url"), null),
),
aMessageContent(
body = "Voice",
type = VoiceMessageType("Voice", MediaSource("url"), null, null),
type = VoiceMessageType("Voice", null, null, MediaSource("url"), null, null),
),
aMessageContent(
body = "Image",
@ -61,11 +61,11 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
),
aMessageContent(
body = "Sticker",
type = StickerMessageType("Image", MediaSource("url"), null),
type = StickerMessageType("Image", null, null, MediaSource("url"), null),
),
aMessageContent(
body = "File",
type = FileMessageType("File", MediaSource("url"), null),
type = FileMessageType("File", null, null, MediaSource("url"), null),
),
aMessageContent(
body = "Location",

View file

@ -15,6 +15,10 @@ import androidx.compose.runtime.produceState
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.canBan
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
import io.element.android.libraries.matrix.api.room.powerlevels.canKick
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
@ -26,6 +30,13 @@ fun MatrixRoom.canSendMessageAsState(type: MessageEventType, updateKey: Long): S
}
}
@Composable
fun MatrixRoom.canInviteAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canInvite().getOrElse { false }
}
}
@Composable
fun MatrixRoom.canRedactOwnAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
@ -54,6 +65,36 @@ fun MatrixRoom.canPinUnpin(updateKey: Long): State<Boolean> {
}
}
@Composable
fun MatrixRoom.isDmAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = isDm
}
}
@Composable
fun MatrixRoom.canKickAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canKick().getOrElse { false }
}
}
@Composable
fun MatrixRoom.canBanAsState(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canBan().getOrElse { false }
}
}
@Composable
fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State<Long> {
return produceState(initialValue = 0, key1 = updateKey) {
value = userRole(sessionId)
.getOrDefault(RoomMember.Role.USER)
.powerLevel
}
}
@Composable
fun MatrixRoom.isOwnUserAdmin(): Boolean {
val roomInfo by roomInfoFlow.collectAsState(initial = null)

View file

@ -75,9 +75,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady(
eventContent = aMessageContent(
messageType = ImageMessageType(
body = "body",
formatted = null,
filename = null,
filename = "filename",
caption = null,
formattedCaption = null,
source = aMediaSource(),
info = anImageInfo(),
)
@ -105,9 +105,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady(
eventContent = aMessageContent(
messageType = ImageMessageType(
body = "body",
formatted = null,
filename = null,
filename = "filename",
caption = "caption",
formattedCaption = null,
source = aMediaSource(),
info = anImageInfo(),
)
@ -134,6 +134,7 @@ class InReplyToMetadataKtTest {
moleculeFlow(RecompositionMode.Immediate) {
anInReplyToDetailsReady(
eventContent = StickerContent(
filename = "filename",
body = "body",
info = anImageInfo(),
source = aMediaSource(url = "url")
@ -160,6 +161,7 @@ class InReplyToMetadataKtTest {
moleculeFlow(RecompositionMode.Immediate) {
anInReplyToDetailsReady(
eventContent = StickerContent(
filename = "filename",
body = "body",
info = anImageInfo(),
source = aMediaSource(url = "url")
@ -187,9 +189,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady(
eventContent = aMessageContent(
messageType = VideoMessageType(
body = "body",
formatted = null,
filename = null,
filename = "filename",
caption = null,
formattedCaption = null,
source = aMediaSource(),
info = aVideoInfo(),
)
@ -217,9 +219,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady(
eventContent = aMessageContent(
messageType = VideoMessageType(
body = "body",
formatted = null,
filename = null,
filename = "filename",
caption = "caption",
formattedCaption = null,
source = aMediaSource(),
info = aVideoInfo(),
)
@ -247,7 +249,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady(
eventContent = aMessageContent(
messageType = FileMessageType(
body = "body",
filename = "filename",
caption = "caption",
formattedCaption = null,
source = aMediaSource(),
info = FileInfo(
mimetype = null,
@ -280,7 +284,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady(
eventContent = aMessageContent(
messageType = FileMessageType(
body = "body",
filename = "filename",
caption = "caption",
formattedCaption = null,
source = aMediaSource(),
info = FileInfo(
mimetype = null,
@ -313,7 +319,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady(
eventContent = aMessageContent(
messageType = AudioMessageType(
body = "body",
filename = "filename",
caption = "caption",
formattedCaption = null,
source = aMediaSource(),
info = AudioInfo(
duration = null,
@ -375,7 +383,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady(
eventContent = aMessageContent(
messageType = VoiceMessageType(
body = "body",
filename = "filename",
caption = "caption",
formattedCaption = null,
source = aMediaSource(),
info = null,
details = null,

View file

@ -303,7 +303,7 @@ private fun MediaFileView(
if (info != null) {
Spacer(modifier = Modifier.height(20.dp))
Text(
text = info.name,
text = info.filename,
maxLines = 2,
style = ElementTheme.typography.fontBodyLgRegular,
overflow = TextOverflow.Ellipsis,

View file

@ -13,43 +13,49 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class MediaInfo(
val name: String,
val filename: String,
val caption: String?,
val mimeType: String,
val formattedFileSize: String,
val fileExtension: String,
) : Parcelable
fun anImageMediaInfo(): MediaInfo = MediaInfo(
"an image file.jpg",
MimeTypes.Jpeg,
"4MB",
"jpg"
filename = "an image file.jpg",
caption = null,
mimeType = MimeTypes.Jpeg,
formattedFileSize = "4MB",
fileExtension = "jpg",
)
fun aVideoMediaInfo(): MediaInfo = MediaInfo(
"a video file.mp4",
MimeTypes.Mp4,
"14MB",
"mp4"
filename = "a video file.mp4",
caption = null,
mimeType = MimeTypes.Mp4,
formattedFileSize = "14MB",
fileExtension = "mp4",
)
fun aPdfMediaInfo(): MediaInfo = MediaInfo(
"a pdf file.pdf",
MimeTypes.Pdf,
"23MB",
"pdf"
filename = "a pdf file.pdf",
caption = null,
mimeType = MimeTypes.Pdf,
formattedFileSize = "23MB",
fileExtension = "pdf",
)
fun anApkMediaInfo(): MediaInfo = MediaInfo(
"an apk file.apk",
MimeTypes.Apk,
"50MB",
"apk"
filename = "an apk file.apk",
caption = null,
mimeType = MimeTypes.Apk,
formattedFileSize = "50MB",
fileExtension = "apk",
)
fun anAudioMediaInfo(): MediaInfo = MediaInfo(
"an audio file.mp3",
MimeTypes.Mp3,
"7MB",
"mp3"
filename = "an audio file.mp3",
caption = null,
mimeType = MimeTypes.Mp3,
formattedFileSize = "7MB",
fileExtension = "mp3",
)

View file

@ -92,7 +92,7 @@ class MediaViewerPresenter @AssistedInject constructor(
mediaLoader.downloadMediaFile(
source = inputs.mediaSource,
mimeType = inputs.mediaInfo.mimeType,
body = inputs.mediaInfo.name
filename = inputs.mediaInfo.filename
)
.onSuccess {
mediaFile.value = it

View file

@ -322,7 +322,7 @@ private fun ThumbnailView(
if (isVisible) {
val mediaRequestData = MediaRequestData(
source = thumbnailSource,
kind = MediaRequestData.Kind.File(mediaInfo.name, mediaInfo.mimeType)
kind = MediaRequestData.Kind.File(mediaInfo.filename, mediaInfo.mimeType)
)
AsyncImage(
modifier = Modifier.fillMaxSize(),

View file

@ -157,7 +157,7 @@ class AndroidLocalMediaActions @Inject constructor(
@RequiresApi(Build.VERSION_CODES.Q)
private fun saveOnDiskUsingMediaStore(localMedia: LocalMedia) {
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, localMedia.info.name)
put(MediaStore.MediaColumns.DISPLAY_NAME, localMedia.info.filename)
put(MediaStore.MediaColumns.MIME_TYPE, localMedia.info.mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
@ -175,7 +175,7 @@ class AndroidLocalMediaActions @Inject constructor(
private fun saveOnDiskUsingExternalStorageApi(localMedia: LocalMedia) {
val target = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
localMedia.info.name
localMedia.info.filename
)
localMedia.openStream()?.use { input ->
FileOutputStream(target).use { output ->

View file

@ -32,21 +32,36 @@ class AndroidLocalMediaFactory @Inject constructor(
private val fileSizeFormatter: FileSizeFormatter,
private val fileExtensionExtractor: FileExtensionExtractor,
) : LocalMediaFactory {
override fun createFromMediaFile(mediaFile: MediaFile, mediaInfo: MediaInfo): LocalMedia {
val uri = mediaFile.toFile().toUri()
return createFromUri(
uri = uri,
mimeType = mediaInfo.mimeType,
name = mediaInfo.name,
formattedFileSize = mediaInfo.formattedFileSize,
)
}
override fun createFromMediaFile(
mediaFile: MediaFile,
mediaInfo: MediaInfo,
): LocalMedia = createFromUri(
uri = mediaFile.toFile().toUri(),
mimeType = mediaInfo.mimeType,
name = mediaInfo.filename,
caption = mediaInfo.caption,
formattedFileSize = mediaInfo.formattedFileSize,
)
override fun createFromUri(
uri: Uri,
mimeType: String?,
name: String?,
formattedFileSize: String?
): LocalMedia = createFromUri(
uri = uri,
mimeType = mimeType,
name = name,
caption = null,
formattedFileSize = formattedFileSize,
)
private fun createFromUri(
uri: Uri,
mimeType: String?,
name: String?,
caption: String?,
formattedFileSize: String?
): LocalMedia {
val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream
val fileName = name ?: context.getFileName(uri) ?: ""
@ -56,7 +71,8 @@ class AndroidLocalMediaFactory @Inject constructor(
uri = uri,
info = MediaInfo(
mimeType = resolvedMimeType,
name = fileName,
filename = fileName,
caption = caption,
formattedFileSize = fileSize,
fileExtension = fileExtension
)

View file

@ -29,7 +29,8 @@ class AndroidLocalMediaFactoryTest {
assertThat(result.uri.toString()).endsWith("aPath")
assertThat(result.info).isEqualTo(
MediaInfo(
name = "an image file.jpg",
filename = "an image file.jpg",
caption = null,
mimeType = MimeTypes.Jpeg,
formattedFileSize = "4MB",
fileExtension = "jpg",

View file

@ -32,7 +32,8 @@ class FakeLocalMediaFactory(
override fun createFromUri(uri: Uri, mimeType: String?, name: String?, formattedFileSize: String?): LocalMedia {
val safeName = name ?: fallbackName
val mediaInfo = MediaInfo(
name = safeName,
filename = safeName,
caption = null,
mimeType = mimeType ?: fallbackMimeType,
formattedFileSize = formattedFileSize ?: fallbackFileSize,
fileExtension = fileExtensionExtractor.extractFromName(safeName)

View file

@ -265,15 +265,15 @@ class DefaultNotifiableEventResolver @Inject constructor(
senderDisambiguatedDisplayName: String,
): String {
return when (val messageType = content.messageType) {
is AudioMessageType -> messageType.body
is AudioMessageType -> messageType.bestDescription
is VoiceMessageType -> stringProvider.getString(CommonStrings.common_voice_message)
is EmoteMessageType -> "* $senderDisambiguatedDisplayName ${messageType.body}"
is FileMessageType -> messageType.body
is ImageMessageType -> messageType.body
is StickerMessageType -> messageType.body
is FileMessageType -> messageType.bestDescription
is ImageMessageType -> messageType.bestDescription
is StickerMessageType -> messageType.bestDescription
is NoticeMessageType -> messageType.body
is TextMessageType -> messageType.toPlainText(permalinkParser = permalinkParser)
is VideoMessageType -> messageType.body
is VideoMessageType -> messageType.bestDescription
is LocationMessageType -> messageType.body
is OtherMessageType -> messageType.body
}
@ -299,7 +299,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
.getMediaFile(
mediaSource = messageType.source,
mimeType = messageType.info?.mimetype,
body = messageType.body,
filename = messageType.filename,
)
is VideoMessageType -> null // Use the thumbnail here?
else -> null

View file

@ -47,13 +47,13 @@ interface NotificationMediaRepo {
*
* @param mediaSource the media source of the media.
* @param mimeType the mime type of the media.
* @param body the body of the message.
* @param filename optional String which will be used to name the file.
* @return A [Result] holding either the media [File] from the cache directory or an [Exception].
*/
suspend fun getMediaFile(
mediaSource: MediaSource,
mimeType: String?,
body: String?,
filename: String?,
): Result<File>
}
@ -75,7 +75,7 @@ class DefaultNotificationMediaRepo @AssistedInject constructor(
override suspend fun getMediaFile(
mediaSource: MediaSource,
mimeType: String?,
body: String?,
filename: String?,
): Result<File> {
val cachedFile = mediaSource.cachedFile()
return when {
@ -84,7 +84,7 @@ class DefaultNotificationMediaRepo @AssistedInject constructor(
else -> matrixMediaLoader.downloadMediaFile(
source = mediaSource,
mimeType = mimeType,
body = body,
filename = filename,
).mapCatching {
it.use { mediaFile ->
val dest = cachedFile.apply { parentFile?.mkdirs() }

View file

@ -187,7 +187,7 @@ class DefaultNotifiableEventResolverTest {
aNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2,
messageType = AudioMessageType(body = "Audio", MediaSource("url"), null)
messageType = AudioMessageType("Audio", null, null, MediaSource("url"), null)
),
)
)
@ -206,7 +206,7 @@ class DefaultNotifiableEventResolverTest {
aNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2,
messageType = VideoMessageType(body = "Video", null, null, MediaSource("url"), null)
messageType = VideoMessageType("Video", null, null, MediaSource("url"), null)
),
)
)
@ -225,7 +225,7 @@ class DefaultNotifiableEventResolverTest {
aNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2,
messageType = VoiceMessageType(body = "Voice", MediaSource("url"), null, null)
messageType = VoiceMessageType("Voice", null, null, MediaSource("url"), null, null)
),
)
)
@ -263,7 +263,7 @@ class DefaultNotifiableEventResolverTest {
aNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2,
messageType = StickerMessageType("Sticker", MediaSource("url"), null),
messageType = StickerMessageType("Sticker", null, null, MediaSource("url"), null),
),
)
)
@ -282,7 +282,7 @@ class DefaultNotifiableEventResolverTest {
aNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2,
messageType = FileMessageType("File", MediaSource("url"), null),
messageType = FileMessageType("File", null, null, MediaSource("url"), null),
),
)
)

View file

@ -15,7 +15,7 @@ class FakeNotificationMediaRepo : NotificationMediaRepo {
override suspend fun getMediaFile(
mediaSource: MediaSource,
mimeType: String?,
body: String?,
filename: String?,
): Result<File> {
return Result.failure(IllegalStateException("Fake class"))
}

View file

@ -25,9 +25,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
@ -37,33 +40,37 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun ComposerModeView(
composerMode: MessageComposerMode,
composerMode: MessageComposerMode.Special,
onResetComposerMode: () -> Unit,
modifier: Modifier = Modifier,
) {
when (composerMode) {
is MessageComposerMode.Edit -> {
EditingModeView(onResetComposerMode = onResetComposerMode)
EditingModeView(
modifier = modifier,
onResetComposerMode = onResetComposerMode,
)
}
is MessageComposerMode.Reply -> {
ReplyToModeView(
modifier = Modifier.padding(8.dp),
modifier = modifier.padding(8.dp),
replyToDetails = composerMode.replyToDetails,
hideImage = composerMode.hideImage,
onResetComposerMode = onResetComposerMode,
)
}
else -> Unit
}
}
@Composable
private fun EditingModeView(
onResetComposerMode: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.padding(start = 12.dp)
) {
@ -124,7 +131,7 @@ private fun ReplyToModeView(
contentDescription = stringResource(CommonStrings.action_close),
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier
.padding(end = 4.dp, top = 4.dp, start = 16.dp, bottom = 16.dp)
.padding(end = 4.dp, top = 4.dp, start = 8.dp, bottom = 16.dp)
.size(16.dp)
.clickable(
enabled = true,
@ -135,3 +142,15 @@ private fun ReplyToModeView(
)
}
}
@PreviewsDayNight
@Composable
internal fun ComposerModeViewPreview(
@PreviewParameter(MessageComposerModeSpecialProvider::class) mode: MessageComposerMode.Special
) = ElementPreview {
ComposerModeView(
composerMode = mode,
onResetComposerMode = {},
modifier = Modifier.background(ElementTheme.colors.bgSubtleSecondary)
)
}

View file

@ -0,0 +1,24 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.textcomposer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsProvider
import io.element.android.libraries.textcomposer.model.MessageComposerMode
class MessageComposerModeSpecialProvider : PreviewParameterProvider<MessageComposerMode.Special> {
override val values: Sequence<MessageComposerMode.Special> = sequenceOf(
aMessageComposerModeEdit()
) +
// Keep only 3 values from InReplyToDetailsProvider
InReplyToDetailsProvider().values.take(3).map {
aMessageComposerModeReply(
replyToDetails = it
)
}
}

View file

@ -42,7 +42,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsProvider
import io.element.android.libraries.testtags.TestTags
@ -432,7 +433,7 @@ private fun TextInputBox(
val defaultTypography = ElementTheme.typography.fontBodyLgRegular
Box(
modifier = Modifier
.padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 42.dp)
.padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp)
// Apply test tag only once, otherwise 2 nodes will have it (both the normal and subcomposing one) and tests will fail
.then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier),
contentAlignment = Alignment.CenterStart,
@ -579,7 +580,7 @@ internal fun TextComposerEditPreview() = ElementPreview {
ATextComposer(
TextEditorState.Rich(aRichTextEditorState(initialText = "A message", initialFocus = true)),
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Edit(EventId("$1234"), TransactionId("1234"), "Some text"),
composerMode = aMessageComposerModeEdit(),
enableVoiceMessages = true,
)
}))
@ -592,7 +593,7 @@ internal fun MarkdownTextComposerEditPreview() = ElementPreview {
ATextComposer(
TextEditorState.Markdown(aMarkdownTextEditorState(initialText = "A message", initialFocus = true)),
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Edit(EventId("$1234"), TransactionId("1234"), "Some text"),
composerMode = aMessageComposerModeEdit(),
enableVoiceMessages = true,
)
}))
@ -604,9 +605,8 @@ internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider
ATextComposer(
state = TextEditorState.Rich(aRichTextEditorState()),
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Reply(
composerMode = aMessageComposerModeReply(
replyToDetails = inReplyToDetails,
hideImage = false,
),
enableVoiceMessages = true,
)
@ -718,3 +718,19 @@ fun aRichTextEditorState(
initialMarkdown = initialMarkdown,
initialFocus = initialFocus,
)
fun aMessageComposerModeEdit(
eventOrTransactionId: EventOrTransactionId = EventId("$1234").toEventOrTransactionId(),
content: String = "Some text",
) = MessageComposerMode.Edit(
eventOrTransactionId = eventOrTransactionId,
content = content
)
fun aMessageComposerModeReply(
replyToDetails: InReplyToDetails,
hideImage: Boolean = false,
) = MessageComposerMode.Reply(
replyToDetails = replyToDetails,
hideImage = hideImage,
)

View file

@ -26,6 +26,8 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.ui.strings.CommonStrings
@ -77,7 +79,7 @@ internal fun SendButton(
@Composable
internal fun SendButtonPreview() = ElementPreview {
val normalMode = MessageComposerMode.Normal
val editMode = MessageComposerMode.Edit(null, null, "")
val editMode = MessageComposerMode.Edit(EventId("\$id").toEventOrTransactionId(), "")
Row {
SendButton(canSendMessage = true, onClick = {}, composerMode = normalMode)
SendButton(canSendMessage = false, onClick = {}, composerMode = normalMode)

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.textcomposer.model
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.eventId
@ -21,8 +21,7 @@ sealed interface MessageComposerMode {
sealed interface Special : MessageComposerMode
data class Edit(
val eventId: EventId?,
val transactionId: TransactionId?,
val eventOrTransactionId: EventOrTransactionId,
val content: String
) : Special
@ -36,7 +35,7 @@ sealed interface MessageComposerMode {
val relatedEventId: EventId?
get() = when (this) {
is Normal -> null
is Edit -> eventId
is Edit -> eventOrTransactionId.eventId
is Reply -> eventId
}

View file

@ -122,7 +122,7 @@ private fun ColumnScope.TroubleshootTestView(
private fun ColumnScope.TroubleshootNotificationsContent(state: TroubleshootNotificationsState) {
when (state.testSuiteState.mainState) {
AsyncAction.Loading,
AsyncAction.Confirming,
is AsyncAction.Confirming,
is AsyncAction.Success,
is AsyncAction.Failure -> {
TestSuiteView(
@ -150,7 +150,7 @@ private fun ColumnScope.TroubleshootNotificationsContent(state: TroubleshootNoti
})
RunTestButton(state = state)
}
AsyncAction.Confirming -> {
is AsyncAction.Confirming -> {
ListItem(headlineContent = {
Text(
text = stringResource(id = R.string.troubleshoot_notifications_screen_waiting)

View file

@ -102,7 +102,7 @@ fun List<NotificationTroubleshootTestState>.computeMainState(): AsyncAction<Unit
isRunning -> AsyncAction.Loading
else -> {
if (any { it.status is NotificationTroubleshootTestState.Status.WaitingForUser }) {
AsyncAction.Confirming
AsyncAction.ConfirmingNoParams
} else if (any { it.status is NotificationTroubleshootTestState.Status.Failure }) {
AsyncAction.Failure(Exception("Some tests failed"))
} else {

View file

@ -106,6 +106,7 @@
<string name="action_send_message">"Адправіць паведамленне"</string>
<string name="action_share">"Падзяліцца"</string>
<string name="action_share_link">"Абагуліць спасылку"</string>
<string name="action_show">"Паказаць"</string>
<string name="action_sign_in_again">"Увайдзіце яшчэ раз"</string>
<string name="action_signout">"Выйсці"</string>
<string name="action_signout_anyway">"Усё роўна выйсці"</string>
@ -132,6 +133,7 @@
<string name="common_call_invite">"Ідзе званок (не падтрымліваецца)"</string>
<string name="common_call_started">"Званок пачаўся"</string>
<string name="common_chat_backup">"Рэзервовае капіраванне чатаў"</string>
<string name="common_copied_to_clipboard">"Скапіравана ў буфер абмену"</string>
<string name="common_copyright">"Аўтарскае права"</string>
<string name="common_creating_room">"Стварэнне пакоя…"</string>
<string name="common_current_user_left_room">"Выйшаў з пакоя"</string>
@ -253,6 +255,7 @@
<string name="common_waiting">"Чакаем…"</string>
<string name="common_waiting_for_decryption_key">"Чакаю гэта паведамленне"</string>
<string name="common_you">"Вы"</string>
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
<string name="dialog_title_confirmation">"Пацвярджэнне"</string>
<string name="dialog_title_error">"Памылка"</string>
<string name="dialog_title_success">"Поспех"</string>
@ -281,6 +284,9 @@
<string name="invite_friends_text">"Гэй, пагавары са мной у %1$s: %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Паведаміць аб памылцы з дапамогай Rageshake"</string>
<string name="screen_create_room_access_section_anyone_option_title">"Хто заўгодна"</string>
<string name="screen_create_room_access_section_header">"Доступ у пакой"</string>
<string name="screen_create_room_access_section_knocking_option_title">"Папрасіце далучыцца"</string>
<string name="screen_media_picker_error_failed_selection">"Не ўдалося выбраць носьбіт, паўтарыце спробу."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз."</string>

View file

@ -66,6 +66,7 @@
<string name="action_forgot_password">"Zapomněli jste heslo?"</string>
<string name="action_forward">"Přeposlat"</string>
<string name="action_go_back">"Přejít zpět"</string>
<string name="action_ignore">"Ignorovat"</string>
<string name="action_invite">"Pozvat"</string>
<string name="action_invite_friends">"Pozvat přátele"</string>
<string name="action_invite_friends_to_app">"Pozvat přátele do %1$s"</string>
@ -140,6 +141,7 @@
<string name="common_dark">"Tmavé"</string>
<string name="common_decryption_error">"Chyba dešifrování"</string>
<string name="common_developer_options">"Možnosti pro vývojáře"</string>
<string name="common_device_id">"ID zařízení"</string>
<string name="common_direct_chat">"Přímý chat"</string>
<string name="common_do_not_show_this_again">"Znovu nezobrazovat"</string>
<string name="common_edited_suffix">"(upraveno)"</string>
@ -249,6 +251,8 @@ Důvod: %1$s."</string>
<string name="common_username">"Uživatelské jméno"</string>
<string name="common_verification_cancelled">"Ověření zrušeno"</string>
<string name="common_verification_complete">"Ověření dokončeno"</string>
<string name="common_verification_failed">"Ověření se nezdařilo"</string>
<string name="common_verified">"Ověřeno"</string>
<string name="common_verify_device">"Ověřit zařízení"</string>
<string name="common_video">"Video"</string>
<string name="common_voice_message">"Hlasová zpráva"</string>
@ -256,6 +260,8 @@ Důvod: %1$s."</string>
<string name="common_waiting_for_decryption_key">"Čekání na dešifrovací klíč"</string>
<string name="common_you">"Vy"</string>
<string name="crypto_identity_change_pin_violation">"Zdá se, že se identita %1$s změnila. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"Zdá se, že identita %1$s %2$s se změnila. %3$s"</string>
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
<string name="dialog_title_confirmation">"Potvrzení"</string>
<string name="dialog_title_error">"Chyba"</string>
<string name="dialog_title_success">"Úspěch"</string>
@ -289,6 +295,13 @@ Důvod: %1$s."</string>
<string name="screen_create_room_access_section_header">"Přístup do místnosti"</string>
<string name="screen_create_room_access_section_knocking_option_description">"Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout"</string>
<string name="screen_create_room_access_section_knocking_option_title">"Požádat o připojení"</string>
<string name="screen_join_room_cancel_knock_action">"Zrušit žádost"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ano, zrušit"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Opravdu chcete zrušit svou žádost o vstup do této místnosti?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Zrušit žádost o vstup"</string>
<string name="screen_join_room_knock_message_description">"Zpráva (nepovinné)"</string>
<string name="screen_join_room_knock_sent_description">"Pokud bude váš požadavek přijat, obdržíte pozvánku na vstup do místnosti."</string>
<string name="screen_join_room_knock_sent_title">"Žádost o vstup odeslána"</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_error_failed_processing">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
@ -320,6 +333,8 @@ Důvod: %1$s."</string>
<string name="screen_room_member_details_unblock_alert_action">"Odblokovat"</string>
<string name="screen_room_member_details_unblock_alert_description">"Znovu uvidíte všechny zprávy od nich."</string>
<string name="screen_room_member_details_unblock_user">"Odblokovat uživatele"</string>
<string name="screen_room_member_details_verify_button_subtitle">"K ověření tohoto uživatele použijte webovou aplikaci."</string>
<string name="screen_room_member_details_verify_button_title">"Ověřit %1$s"</string>
<string name="screen_room_pinned_banner_indicator">"%1$s z %2$s"</string>
<string name="screen_room_pinned_banner_indicator_description">"%1$s Připnuté zprávy"</string>
<string name="screen_room_pinned_banner_loading_description">"Načítání zprávy…"</string>

View file

@ -245,6 +245,7 @@
<string name="common_username">"Όνομα χρήστη"</string>
<string name="common_verification_cancelled">"Η επαλήθευση ακυρώθηκε"</string>
<string name="common_verification_complete">"Η επαλήθευση ολοκληρώθηκε"</string>
<string name="common_verified">"Επαληθεύτηκε"</string>
<string name="common_verify_device">"Επαλήθευση συσκευής"</string>
<string name="common_video">"Βίντεο"</string>
<string name="common_voice_message">"Φωνητικό μήνυμα"</string>
@ -252,6 +253,8 @@
<string name="common_waiting_for_decryption_key">"Αναμονή για αυτό το μήνυμα"</string>
<string name="common_you">"Εσύ"</string>
<string name="crypto_identity_change_pin_violation">"Η ταυτότητα του χρήστη %1$s φαίνεται να έχει αλλάξει. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"Η ταυτότητα του %1$s %2$s φαίνεται να έχει αλλάξει. %3$s"</string>
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
<string name="dialog_title_confirmation">"Επιβεβαίωση"</string>
<string name="dialog_title_error">"Σφάλμα"</string>
<string name="dialog_title_success">"Επιτυχία"</string>
@ -280,6 +283,12 @@
<string name="invite_friends_text">"Γεια, μίλα μου στην εφαρμογή %1$s :%2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Κούνησε δυνατά τη συσκευή σου για να αναφέρεις κάποιο σφάλμα"</string>
<string name="screen_create_room_access_section_anyone_option_description">"Οποιοσδήποτε μπορεί να συμμετάσχει σε αυτό το δωμάτιο"</string>
<string name="screen_create_room_access_section_anyone_option_title">"Οποιοσδήποτε"</string>
<string name="screen_create_room_access_section_header">"Πρόσβαση Δωματίου"</string>
<string name="screen_create_room_access_section_knocking_option_description">"Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στο δωμάτιο, αλλά ένας διαχειριστής ή συντονιστής θα πρέπει να αποδεχθεί το αίτημα"</string>
<string name="screen_create_room_access_section_knocking_option_title">"Αίτημα συμμετοχής"</string>
<string name="screen_join_room_knock_message_description">"Μήνυμα (προαιρετικό)"</string>
<string name="screen_media_picker_error_failed_selection">"Αποτυχία επιλογής πολυμέσου, δοκίμασε ξανά."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Αποτυχία μεταφόρτωσης πολυμέσων, δοκίμασε ξανά."</string>

View file

@ -64,6 +64,7 @@
<string name="action_forgot_password">"Kas unustasid salasõna?"</string>
<string name="action_forward">"Edasta"</string>
<string name="action_go_back">"Tagasi eelmisesse vaatesse"</string>
<string name="action_ignore">"Eira"</string>
<string name="action_invite">"Kutsu"</string>
<string name="action_invite_friends">"Kutsu osalejaid"</string>
<string name="action_invite_friends_to_app">"Kutsu huvilisi kasutama rakendust %1$s"</string>
@ -138,6 +139,7 @@
<string name="common_dark">"Tume"</string>
<string name="common_decryption_error">"Dekrüptimisviga"</string>
<string name="common_developer_options">"Arendaja valikud"</string>
<string name="common_device_id">"Seadme tunnus"</string>
<string name="common_direct_chat">"Otsevestlus"</string>
<string name="common_do_not_show_this_again">"Ära enam näita seda uuesti"</string>
<string name="common_edited_suffix">"(muudetud)"</string>
@ -245,6 +247,8 @@ Põhjus: %1$s."</string>
<string name="common_username">"Kasutajanimi"</string>
<string name="common_verification_cancelled">"Verifitseerimine on katkestatud"</string>
<string name="common_verification_complete">"Verifitseerimine on tehtud"</string>
<string name="common_verification_failed">"Verifitseerimine ei õnnestunud"</string>
<string name="common_verified">"Verifitseeritud"</string>
<string name="common_verify_device">"Verifitseeri seade"</string>
<string name="common_video">"Video"</string>
<string name="common_voice_message">"Häälsõnum"</string>
@ -252,6 +256,8 @@ Põhjus: %1$s."</string>
<string name="common_waiting_for_decryption_key">"Ootame selle sõnumi dekrüptimisvõtit"</string>
<string name="common_you">"Sina"</string>
<string name="crypto_identity_change_pin_violation">"Kasutaja %1$s võrguidentiteet tundub olema muutunud. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"Kasutaja %1$s %2$s võrguidentiteet tundub olema muutunud. %3$s"</string>
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
<string name="dialog_title_confirmation">"Kinnitus"</string>
<string name="dialog_title_error">"Viga"</string>
<string name="dialog_title_success">"Õnnestus"</string>
@ -285,6 +291,13 @@ Põhjus: %1$s."</string>
<string name="screen_create_room_access_section_header">"Ligipääs jututoale"</string>
<string name="screen_create_room_access_section_knocking_option_description">"Kõik võivad paluda selle jututoaga liitumist, kuid peakasutaja või moderaator peavad selle kinnitama"</string>
<string name="screen_create_room_access_section_knocking_option_title">"Küsi võimalust liitumiseks"</string>
<string name="screen_join_room_cancel_knock_action">"Tühista liitumispalve"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Jah, tühista"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Kas sa oled kindel, et soovid tühistada oma palve jututoaga liitumiseks?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Tühista liitumispalve"</string>
<string name="screen_join_room_knock_message_description">"Selgitus (kui soovid lisada)"</string>
<string name="screen_join_room_knock_sent_description">"Kui sinu liitumispalvega ollakse nõus, siis saad kutse jututoaga liitumiseks."</string>
<string name="screen_join_room_knock_sent_title">"Liitumispalve on saadetud"</string>
<string name="screen_media_picker_error_failed_selection">"Meediafaili valimine ei õnnestunud. Palun proovi uuesti."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Meediafaili üleslaadimine ei õnnestunud. Palun proovi uuesti."</string>
@ -315,6 +328,8 @@ Põhjus: %1$s."</string>
<string name="screen_room_member_details_unblock_alert_action">"Eemalda blokeering"</string>
<string name="screen_room_member_details_unblock_alert_description">"Nüüd näed sa jälle kõiki tema sõnumeid"</string>
<string name="screen_room_member_details_unblock_user">"Eemalda kasutajalt blokeering"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Kasutaja verifitseerimiseks kasuta veebirakendust."</string>
<string name="screen_room_member_details_verify_button_title">"Verifitseeri kasutaja %1$s"</string>
<string name="screen_room_pinned_banner_indicator">"%1$s / %2$s"</string>
<string name="screen_room_pinned_banner_indicator_description">"%1$s esiletõstetud sõnumit"</string>
<string name="screen_room_pinned_banner_loading_description">"Laadime sõnumit…"</string>

View file

@ -64,6 +64,7 @@
<string name="action_forgot_password">"Mot de passe oublié ?"</string>
<string name="action_forward">"Transférer"</string>
<string name="action_go_back">"Retour"</string>
<string name="action_ignore">"Ignorer"</string>
<string name="action_invite">"Inviter"</string>
<string name="action_invite_friends">"Inviter des amis"</string>
<string name="action_invite_friends_to_app">"Inviter des amis à %1$s"</string>
@ -138,6 +139,7 @@
<string name="common_dark">"Sombre"</string>
<string name="common_decryption_error">"Erreur de déchiffrement"</string>
<string name="common_developer_options">"Options pour les développeurs"</string>
<string name="common_device_id">"Identifiant de session"</string>
<string name="common_direct_chat">"Discussion à deux"</string>
<string name="common_do_not_show_this_again">"Ne plus afficher"</string>
<string name="common_edited_suffix">"(modifié)"</string>
@ -245,6 +247,7 @@ Raison: %1$s."</string>
<string name="common_username">"Nom dutilisateur"</string>
<string name="common_verification_cancelled">"Vérification annulée"</string>
<string name="common_verification_complete">"Vérification terminée"</string>
<string name="common_verified">"Vérifié(e)"</string>
<string name="common_verify_device">"Vérifier la session"</string>
<string name="common_video">"Vidéo"</string>
<string name="common_voice_message">"Message vocal"</string>
@ -252,6 +255,8 @@ Raison: %1$s."</string>
<string name="common_waiting_for_decryption_key">"En attente de la clé de déchiffrement"</string>
<string name="common_you">"Vous"</string>
<string name="crypto_identity_change_pin_violation">"Lidentité de %1$s semble avoir changé. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"Lidentité de %1$s %2$s semble avoir changé. %3$s"</string>
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
<string name="dialog_title_confirmation">"Confirmation"</string>
<string name="dialog_title_error">"Erreur"</string>
<string name="dialog_title_success">"Succès"</string>
@ -285,6 +290,10 @@ Raison: %1$s."</string>
<string name="screen_create_room_access_section_header">"Accès au salon"</string>
<string name="screen_create_room_access_section_knocking_option_description">"Tout le monde peut demander à rejoindre le salon, mais un administrateur ou un modérateur devra accepter la demande"</string>
<string name="screen_create_room_access_section_knocking_option_title">"Demander à rejoindre"</string>
<string name="screen_join_room_cancel_knock_action">"Annuler la demande"</string>
<string name="screen_join_room_knock_message_description">"Message (facultatif)"</string>
<string name="screen_join_room_knock_sent_description">"Vous recevrez une invitation à rejoindre le salon si votre demande est acceptée."</string>
<string name="screen_join_room_knock_sent_title">"Demande de rejoindre le salon envoyée"</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_error_failed_processing">"Échec du traitement des médias à télécharger, veuillez réessayer."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Échec du téléchargement du média, veuillez réessayer."</string>
@ -315,6 +324,8 @@ Raison: %1$s."</string>
<string name="screen_room_member_details_unblock_alert_action">"Débloquer"</string>
<string name="screen_room_member_details_unblock_alert_description">"Vous pourrez à nouveau voir tous ses messages."</string>
<string name="screen_room_member_details_unblock_user">"Débloquer lutilisateur"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Utilisez lapplication Web pour vérifier cet utilisateur."</string>
<string name="screen_room_member_details_verify_button_title">"Vérifier %1$s"</string>
<string name="screen_room_pinned_banner_indicator">"%1$s sur %2$s"</string>
<string name="screen_room_pinned_banner_indicator_description">"%1$s Messages épinglés"</string>
<string name="screen_room_pinned_banner_loading_description">"Chargement du message…"</string>

View file

@ -64,6 +64,7 @@
<string name="action_forgot_password">"Elfelejtette a jelszót?"</string>
<string name="action_forward">"Tovább"</string>
<string name="action_go_back">"Visszalépés"</string>
<string name="action_ignore">"Mellőzés"</string>
<string name="action_invite">"Meghívás"</string>
<string name="action_invite_friends">"Ismerősök meghívása"</string>
<string name="action_invite_friends_to_app">"Ismerősök meghívása ide: %1$s"</string>
@ -138,6 +139,7 @@
<string name="common_dark">"Sötét"</string>
<string name="common_decryption_error">"Visszafejtési hiba"</string>
<string name="common_developer_options">"Fejlesztői beállítások"</string>
<string name="common_device_id">"Eszközazonosító"</string>
<string name="common_direct_chat">"Közvetlen csevegés"</string>
<string name="common_do_not_show_this_again">"Ne jelenjen meg többé"</string>
<string name="common_edited_suffix">"(szerkesztve)"</string>
@ -245,6 +247,8 @@ Ok: %1$s."</string>
<string name="common_username">"Felhasználónév"</string>
<string name="common_verification_cancelled">"Az ellenőrzés megszakítva"</string>
<string name="common_verification_complete">"Az ellenőrzés befejeződött"</string>
<string name="common_verification_failed">"Az ellenőrzés sikertelen"</string>
<string name="common_verified">"Ellenőrizve"</string>
<string name="common_verify_device">"Eszköz ellenőrzése"</string>
<string name="common_video">"Videó"</string>
<string name="common_voice_message">"Hangüzenet"</string>
@ -252,6 +256,8 @@ Ok: %1$s."</string>
<string name="common_waiting_for_decryption_key">"Várakozás a visszafejtési kulcsra"</string>
<string name="common_you">"Ön"</string>
<string name="crypto_identity_change_pin_violation">"Úgy tűnik, hogy %1$s személyazonossága megváltozott. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"Úgy tűnik, hogy %1$s %2$s személyazonossága megváltozott. %3$s"</string>
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
<string name="dialog_title_confirmation">"Megerősítés"</string>
<string name="dialog_title_error">"Hiba"</string>
<string name="dialog_title_success">"Sikeres"</string>
@ -285,6 +291,13 @@ Ok: %1$s."</string>
<string name="screen_create_room_access_section_header">"Szobahozzáférés"</string>
<string name="screen_create_room_access_section_knocking_option_description">"Bárki kérheti, hogy csatlakozzon a szobához, de egy adminisztrátornak vagy moderátornak el kell fogadnia a kérést"</string>
<string name="screen_create_room_access_section_knocking_option_title">"Csatlakozás kérése"</string>
<string name="screen_join_room_cancel_knock_action">"Kérés visszavonása"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Igen, visszavonás"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Biztos, hogy visszavonja a szobához való csatlakozási kérését?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Csatlakozási kérés visszavonása"</string>
<string name="screen_join_room_knock_message_description">"Üzenet (nem kötelező)"</string>
<string name="screen_join_room_knock_sent_description">"Ha a kérését elfogadják, meghívót kap a szobához való csatlakozáshoz."</string>
<string name="screen_join_room_knock_sent_title">"Csatlakozási kérés elküldve"</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_error_failed_processing">"Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Nem sikerült a média feltöltése, próbálja újra."</string>
@ -315,6 +328,8 @@ Ok: %1$s."</string>
<string name="screen_room_member_details_unblock_alert_action">"Letiltás feloldása"</string>
<string name="screen_room_member_details_unblock_alert_description">"Újra láthatja az összes üzenetét."</string>
<string name="screen_room_member_details_unblock_user">"Felhasználó kitiltásának feloldása"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Használja a webes alkalmazást a felhasználó ellenőrzéséhez."</string>
<string name="screen_room_member_details_verify_button_title">"A(z) %1$s ellenőrzése"</string>
<string name="screen_room_pinned_banner_indicator">"%1$s / %2$s"</string>
<string name="screen_room_pinned_banner_indicator_description">"%1$s kitűzött üzenet"</string>
<string name="screen_room_pinned_banner_loading_description">"Üzenet betöltése…"</string>

View file

@ -64,6 +64,7 @@
<string name="action_forgot_password">"Esqueceu-se da senha?"</string>
<string name="action_forward">"Reencaminhar"</string>
<string name="action_go_back">"Voltar"</string>
<string name="action_ignore">"Ignorar"</string>
<string name="action_invite">"Convidar"</string>
<string name="action_invite_friends">"Convidar pessoas"</string>
<string name="action_invite_friends_to_app">"Convidar amigos para %1$s"</string>
@ -138,6 +139,7 @@
<string name="common_dark">"Escuro"</string>
<string name="common_decryption_error">"Erro de decifragem"</string>
<string name="common_developer_options">"Opções de programador"</string>
<string name="common_device_id">"ID do dispositivo"</string>
<string name="common_direct_chat">"Conversa direta"</string>
<string name="common_do_not_show_this_again">"Não mostrar novamente"</string>
<string name="common_edited_suffix">"(editada)"</string>
@ -185,7 +187,7 @@ Razão: %1$s."</string>
<string name="common_permalink">"Ligação permanente"</string>
<string name="common_permission">"Permissão"</string>
<string name="common_pinned">"Afixado"</string>
<string name="common_please_wait">"Por favor, aguarda…"</string>
<string name="common_please_wait">"Por favor, aguarde…"</string>
<string name="common_poll_end_confirmation">"Tens a certeza que queres concluir esta sondagem?"</string>
<string name="common_poll_summary">"Sondagem: %1$s"</string>
<string name="common_poll_total_votes">"Total de votos: %1$s"</string>
@ -245,6 +247,7 @@ Razão: %1$s."</string>
<string name="common_username">"Nome de utilizador"</string>
<string name="common_verification_cancelled">"Verificação cancelada"</string>
<string name="common_verification_complete">"Verificação concluída"</string>
<string name="common_verified">"Verificado"</string>
<string name="common_verify_device">"Verificar o dispositivo"</string>
<string name="common_video">"Vídeo"</string>
<string name="common_voice_message">"Mensagem de voz"</string>
@ -252,6 +255,8 @@ Razão: %1$s."</string>
<string name="common_waiting_for_decryption_key">"À espera desta mensagem"</string>
<string name="common_you">"Você"</string>
<string name="crypto_identity_change_pin_violation">"A identidade de %1$s parece ter mudado. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"A identidade de %1$s (username: %2$s ) aparenta ter mudado. %3$s"</string>
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
<string name="dialog_title_confirmation">"Confirmação"</string>
<string name="dialog_title_error">"Erro"</string>
<string name="dialog_title_success">"Sucesso"</string>
@ -269,7 +274,7 @@ Razão: %1$s."</string>
<string name="error_missing_location_rationale_android">"A %1$s não tem permissão para aceder à tua localização. Continua para ativares o acesso."</string>
<string name="error_missing_microphone_voice_rationale_android">"A %1$s não tem permissão para aceder ao teu microfone. Permite o acesso para gravar uma mensagem de voz."</string>
<string name="error_some_messages_have_not_been_sent">"Algumas mensagens não foram enviadas"</string>
<string name="error_unknown">"Ocorreu um erro, desculpa"</string>
<string name="error_unknown">"Desculpe, ocorreu um erro"</string>
<string name="event_shield_reason_authenticity_not_guaranteed">"A autenticidade desta mensagem cifrada não pode ser garantida neste dispositivo."</string>
<string name="event_shield_reason_previously_verified">"Criptografado por um usuário verificado anteriormente."</string>
<string name="event_shield_reason_sent_in_clear">"Não cifrado."</string>
@ -280,6 +285,15 @@ Razão: %1$s."</string>
<string name="invite_friends_text">"Alô! Fala comigo na %1$s: %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Agita o dispositivo em fúria para comunicar um problema"</string>
<string name="screen_create_room_access_section_anyone_option_description">"Qualquer pessoa pode entrar nesta sala"</string>
<string name="screen_create_room_access_section_anyone_option_title">"Qualquer pessoa"</string>
<string name="screen_create_room_access_section_header">"Acesso à sala"</string>
<string name="screen_create_room_access_section_knocking_option_description">"Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou um moderador terá de aceitar o pedido"</string>
<string name="screen_create_room_access_section_knocking_option_title">"Pedir para participar"</string>
<string name="screen_join_room_cancel_knock_action">"Cancelar pedido"</string>
<string name="screen_join_room_knock_message_description">"Mensagem (opcional)"</string>
<string name="screen_join_room_knock_sent_description">"Irá receber um convite para participar na sala se seu pedido for aceite."</string>
<string name="screen_join_room_knock_sent_title">"Pedido de adesão enviado"</string>
<string name="screen_media_picker_error_failed_selection">"Falha ao selecionar multimédia, por favor tente novamente."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Falha ao processar multimédia para carregamento, por favor tente novamente."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Falhar ao carregar multimédia, por favor tente novamente."</string>
@ -310,6 +324,8 @@ Razão: %1$s."</string>
<string name="screen_room_member_details_unblock_alert_action">"Desbloquear"</string>
<string name="screen_room_member_details_unblock_alert_description">"Poderás voltar a ver todas as suas mensagens."</string>
<string name="screen_room_member_details_unblock_user">"Desbloquear utilizador"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Utiliza a aplicação Web para verificar este utilizador."</string>
<string name="screen_room_member_details_verify_button_title">"Verifique %1$s"</string>
<string name="screen_room_pinned_banner_indicator">"%1$s de %2$s"</string>
<string name="screen_room_pinned_banner_indicator_description">"%1$s mensagens afixadas"</string>
<string name="screen_room_pinned_banner_loading_description">"A carregar mensagem…"</string>

View file

@ -66,6 +66,7 @@
<string name="action_forgot_password">"Забыли пароль?"</string>
<string name="action_forward">"Переслать"</string>
<string name="action_go_back">"Вернуться"</string>
<string name="action_ignore">"Игнорировать"</string>
<string name="action_invite">"Пригласить"</string>
<string name="action_invite_friends">"Пригласить в комнату"</string>
<string name="action_invite_friends_to_app">"Пригласить в %1$s"</string>
@ -73,7 +74,7 @@
<string name="action_invites_list">"Приглашения"</string>
<string name="action_join">"Присоединиться"</string>
<string name="action_learn_more">"Подробнее"</string>
<string name="action_leave">"Выйти"</string>
<string name="action_leave">"Покинуть"</string>
<string name="action_leave_conversation">"Покинуть беседу"</string>
<string name="action_leave_room">"Покинуть комнату"</string>
<string name="action_load_more">"Загрузить еще"</string>
@ -106,6 +107,7 @@
<string name="action_send_message">"Отправить сообщение"</string>
<string name="action_share">"Поделиться"</string>
<string name="action_share_link">"Поделиться ссылкой"</string>
<string name="action_show">"Показать"</string>
<string name="action_sign_in_again">"Повторите вход"</string>
<string name="action_signout">"Выйти"</string>
<string name="action_signout_anyway">"Все равно выйти"</string>
@ -132,12 +134,14 @@
<string name="common_call_invite">"Выполняется звонок (не поддерживается)"</string>
<string name="common_call_started">"Звонок начат"</string>
<string name="common_chat_backup">"Резервная копия чатов"</string>
<string name="common_copied_to_clipboard">"Скопировано в буфер обмена"</string>
<string name="common_copyright">"Авторское право"</string>
<string name="common_creating_room">"Создание комнаты…"</string>
<string name="common_current_user_left_room">"Покинул комнату"</string>
<string name="common_dark">"Темная"</string>
<string name="common_dark">"Тёмное"</string>
<string name="common_decryption_error">"Ошибка расшифровки"</string>
<string name="common_developer_options">"Для разработчика"</string>
<string name="common_device_id">"Идентификатор устройства"</string>
<string name="common_direct_chat">"Личный чат"</string>
<string name="common_do_not_show_this_again">"Не показывать больше"</string>
<string name="common_edited_suffix">"(изменено)"</string>
@ -146,10 +150,10 @@
<string name="common_encryption_enabled">"Шифрование включено"</string>
<string name="common_enter_your_pin">"Введите свой PIN-код"</string>
<string name="common_error">"Ошибка"</string>
<string name="common_error_registering_pusher_android">"Произошла ошибка, возможно, вы не будете получать уведомления о новых сообщениях. Устраните неполадки с уведомлениями в настройках.
<string name="common_error_registering_pusher_android">"Произошла ошибка. Вы можете не получать уведомления о новых сообщениях. Устраните неполадки с уведомлениями в настройках.
Причина:%1$s."</string>
<string name="common_everyone">"Для всех"</string>
Причина: %1$s."</string>
<string name="common_everyone">"Все"</string>
<string name="common_failed">"Ошибка"</string>
<string name="common_favourite">"Избранное"</string>
<string name="common_favourited">"Избранное"</string>
@ -161,8 +165,8 @@
<string name="common_in_reply_to">"В ответ на %1$s"</string>
<string name="common_install_apk_android">"Установить APK"</string>
<string name="common_invite_unknown_profile">"Идентификатор Matrix ID не найден, приглашение может быть не получено."</string>
<string name="common_leaving_room">"Покинуть комнату"</string>
<string name="common_light">"Светлая"</string>
<string name="common_leaving_room">"Покидание комнаты"</string>
<string name="common_light">"Светлое"</string>
<string name="common_link_copied_to_clipboard">"Ссылка скопирована в буфер обмена"</string>
<string name="common_loading">"Загрузка…"</string>
<plurals name="common_member_count">
@ -175,9 +179,9 @@
<string name="common_message_layout">"Оформление сообщения"</string>
<string name="common_message_removed">"Сообщение удалено"</string>
<string name="common_modern">"Современный"</string>
<string name="common_mute">"Без звука"</string>
<string name="common_mute">"Выкл. звук"</string>
<string name="common_no_results">"Ничего не найдено"</string>
<string name="common_no_room_name">"Нету названия комнаты"</string>
<string name="common_no_room_name">"Название комнаты отсутствует"</string>
<string name="common_offline">"Не в сети"</string>
<string name="common_open_source_licenses">"Лицензии с открытым исходным кодом"</string>
<string name="common_or">"или"</string>
@ -210,11 +214,11 @@
<string name="common_rich_text_editor">"Редактор форматированного текста"</string>
<string name="common_room">"Комната"</string>
<string name="common_room_name">"Название комнаты"</string>
<string name="common_room_name_placeholder">"напр., название вашего проекта"</string>
<string name="common_saved_changes">"Сохраненные изменения"</string>
<string name="common_room_name_placeholder">"например, название вашего проекта"</string>
<string name="common_saved_changes">"Изменения сохранены"</string>
<string name="common_saving">"Сохранение"</string>
<string name="common_screen_lock">"Блокировка экрана"</string>
<string name="common_search_for_someone">"Поиск человека"</string>
<string name="common_screen_lock">"Блокировка приложения"</string>
<string name="common_search_for_someone">"Найти кого-нибудь"</string>
<string name="common_search_results">"Результаты поиска"</string>
<string name="common_security">"Безопасность"</string>
<string name="common_seen_by">"Просмотрено"</string>
@ -225,15 +229,15 @@
<string name="common_server_not_supported">"Сервер не поддерживается"</string>
<string name="common_server_url">"Адрес сервера"</string>
<string name="common_settings">"Настройки"</string>
<string name="common_shared_location">"Делится местонахождением"</string>
<string name="common_shared_location">"Поделился местоположением"</string>
<string name="common_signing_out">"Выход…"</string>
<string name="common_something_went_wrong">"Что-то пошло не так"</string>
<string name="common_starting_chat">"Начало чата…"</string>
<string name="common_starting_chat">"Чат запускается…"</string>
<string name="common_sticker">"Стикер"</string>
<string name="common_success">"Успешно"</string>
<string name="common_suggestions">"Предложения"</string>
<string name="common_syncing">"Синхронизация"</string>
<string name="common_system">"Системная"</string>
<string name="common_system">"Системное"</string>
<string name="common_text">"Текст"</string>
<string name="common_third_party_notices">"Уведомление о третьей стороне"</string>
<string name="common_thread">"Обсуждение"</string>
@ -244,23 +248,29 @@
<string name="common_unable_to_invite_message">"Не удалось отправить приглашения одному или нескольким пользователям."</string>
<string name="common_unable_to_invite_title">"Не удалось отправить приглашение(я)"</string>
<string name="common_unlock">"Разблокировать"</string>
<string name="common_unmute">"Включить звук"</string>
<string name="common_unmute">"Вкл. звук"</string>
<string name="common_unsupported_event">"Неподдерживаемое событие"</string>
<string name="common_username">"Имя пользователя"</string>
<string name="common_verification_cancelled">"Проверка отменена"</string>
<string name="common_verification_complete">"Проверка завершена"</string>
<string name="common_verification_failed">"Сбой проверки"</string>
<string name="common_verified">"Проверено"</string>
<string name="common_verify_device">"Подтверждение устройства"</string>
<string name="common_video">"Видео"</string>
<string name="common_voice_message">"Голосовое сообщение"</string>
<string name="common_waiting">"Ожидание…"</string>
<string name="common_waiting_for_decryption_key">"Ожидание ключа расшифровки"</string>
<string name="common_you">"Вы"</string>
<string name="crypto_identity_change_pin_violation">"Судя по всему, идентификатор %1$s изменился. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"Пользователь %1$s сменил имя пользователя на %2$s. %3$s"</string>
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
<string name="dialog_title_confirmation">"Подтверждение"</string>
<string name="dialog_title_error">"Ошибка"</string>
<string name="dialog_title_success">"Успешно"</string>
<string name="dialog_title_warning">"Предупреждение"</string>
<string name="dialog_unsaved_changes_description_android">"Изменения не сохранены. Вы действительно хотите вернуться?"</string>
<string name="dialog_unsaved_changes_title">"Сохранить изменения?"</string>
<string name="error_account_creation_not_possible">"Ваш homeserver необходимо обновить, чтобы он поддерживал Matrix Authentication Service и создание учетной записи."</string>
<string name="error_account_creation_not_possible">"Ваш домашний сервер необходимо обновить, чтобы он поддерживал Matrix Authentication Service и создание учётных записей."</string>
<string name="error_failed_creating_the_permalink">"Не удалось создать постоянную ссылку"</string>
<string name="error_failed_loading_map">"Не удалось загрузить карту %1$s. Пожалуйста, повторите попытку позже."</string>
<string name="error_failed_loading_messages">"Не удалось загрузить сообщения"</string>
@ -282,15 +292,27 @@
<string name="invite_friends_text">"Привет, поговори со мной по %1$s: %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Встряхните устройство, чтобы сообщить об ошибке"</string>
<string name="screen_create_room_access_section_anyone_option_description">"Любой желающий может присоединиться к этой комнате"</string>
<string name="screen_create_room_access_section_anyone_option_title">"Любой"</string>
<string name="screen_create_room_access_section_header">"Доступ в комнату"</string>
<string name="screen_create_room_access_section_knocking_option_description">"Любой желающий может подать заявку на присоединение к комнате, но администратор или модератор должен будет принять запрос."</string>
<string name="screen_create_room_access_section_knocking_option_title">"Попросить присоединиться"</string>
<string name="screen_join_room_cancel_knock_action">"Отменить запрос"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Да, отменить"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Вы действительно хотите отменить заявку на вступление в эту комнату?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Отменить запрос на присоединение"</string>
<string name="screen_join_room_knock_message_description">"Сообщение (опционально)"</string>
<string name="screen_join_room_knock_sent_description">"Вы получите приглашение присоединиться к комнате, как только ваш запрос будет принят."</string>
<string name="screen_join_room_knock_sent_title">"Запрос на присоединение отправлен"</string>
<string name="screen_media_picker_error_failed_selection">"Не удалось выбрать носитель, попробуйте еще раз."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Не удалось загрузить медиафайлы, попробуйте еще раз."</string>
<string name="screen_pinned_timeline_empty_state_description">"Нажмите на сообщение и выберите “%1$s”, чтобы добавить его сюда."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Закрепите важные сообщения, чтобы их можно было легко найти"</string>
<plurals name="screen_pinned_timeline_screen_title">
<item quantity="one">"%1$d Закрепленное сообщение"</item>
<item quantity="few">"%1$d Закрепленных сообщений"</item>
<item quantity="many">"%1$d Закрепленных сообщений"</item>
<item quantity="one">"%1$d закреплённое сообщение"</item>
<item quantity="few">"%1$d закреплённых сообщения"</item>
<item quantity="many">"%1$d закреплённых сообщений"</item>
</plurals>
<string name="screen_pinned_timeline_screen_title_empty">"Закрепленные сообщения"</string>
<string name="screen_reset_identity_confirmation_subtitle">"Вы собираетесь перейти в свою учетную запись %1$s, чтобы сбросить идентификацию. После этого вы вернетесь в приложение."</string>
@ -313,6 +335,8 @@
<string name="screen_room_member_details_unblock_alert_action">"Разблокировать"</string>
<string name="screen_room_member_details_unblock_alert_description">"Вы снова сможете увидеть все сообщения."</string>
<string name="screen_room_member_details_unblock_user">"Разблокировать пользователя"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Используйте веб-приложение для проверки этого пользователя."</string>
<string name="screen_room_member_details_verify_button_title">"Верифицировать %1$s"</string>
<string name="screen_room_pinned_banner_indicator">"%1$s из %2$s"</string>
<string name="screen_room_pinned_banner_indicator_description">"%1$s Закрепленные сообщения"</string>
<string name="screen_room_pinned_banner_loading_description">"Загрузка сообщения…"</string>

View file

@ -64,6 +64,7 @@
<string name="action_forgot_password">"Forgot password?"</string>
<string name="action_forward">"Forward"</string>
<string name="action_go_back">"Go back"</string>
<string name="action_ignore">"Ignore"</string>
<string name="action_invite">"Invite"</string>
<string name="action_invite_friends">"Invite people"</string>
<string name="action_invite_friends_to_app">"Invite people to %1$s"</string>
@ -138,6 +139,7 @@
<string name="common_dark">"Dark"</string>
<string name="common_decryption_error">"Decryption error"</string>
<string name="common_developer_options">"Developer options"</string>
<string name="common_device_id">"Device ID"</string>
<string name="common_direct_chat">"Direct chat"</string>
<string name="common_do_not_show_this_again">"Do not show this again"</string>
<string name="common_edited_suffix">"(edited)"</string>
@ -245,6 +247,8 @@ Reason: %1$s."</string>
<string name="common_username">"Username"</string>
<string name="common_verification_cancelled">"Verification cancelled"</string>
<string name="common_verification_complete">"Verification complete"</string>
<string name="common_verification_failed">"Verification failed"</string>
<string name="common_verified">"Verified"</string>
<string name="common_verify_device">"Verify device"</string>
<string name="common_video">"Video"</string>
<string name="common_voice_message">"Voice message"</string>
@ -252,6 +256,8 @@ Reason: %1$s."</string>
<string name="common_waiting_for_decryption_key">"Waiting for this message"</string>
<string name="common_you">"You"</string>
<string name="crypto_identity_change_pin_violation">"%1$s\'s identity appears to have changed. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"%1$ss %2$s identity appears to have changed. %3$s"</string>
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
<string name="dialog_title_confirmation">"Confirmation"</string>
<string name="dialog_title_error">"Error"</string>
<string name="dialog_title_success">"Success"</string>
@ -285,6 +291,13 @@ Reason: %1$s."</string>
<string name="screen_create_room_access_section_header">"Room Access"</string>
<string name="screen_create_room_access_section_knocking_option_description">"Anyone can ask to join the room but an administrator or a moderator will have to accept the request"</string>
<string name="screen_create_room_access_section_knocking_option_title">"Ask to join"</string>
<string name="screen_join_room_cancel_knock_action">"Cancel request"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Yes, cancel"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Are you sure that you want to cancel your request to join this room?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Cancel request to join"</string>
<string name="screen_join_room_knock_message_description">"Message (optional)"</string>
<string name="screen_join_room_knock_sent_description">"You will receive an invite to join the room if your request is accepted."</string>
<string name="screen_join_room_knock_sent_title">"Request to join sent"</string>
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
@ -315,6 +328,8 @@ Reason: %1$s."</string>
<string name="screen_room_member_details_unblock_alert_action">"Unblock"</string>
<string name="screen_room_member_details_unblock_alert_description">"You\'ll be able to see all messages from them again."</string>
<string name="screen_room_member_details_unblock_user">"Unblock user"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Use the web app to verify this user."</string>
<string name="screen_room_member_details_verify_button_title">"Verify %1$s"</string>
<string name="screen_room_pinned_banner_indicator">"%1$s of %2$s"</string>
<string name="screen_room_pinned_banner_indicator_description">"%1$s Pinned messages"</string>
<string name="screen_room_pinned_banner_loading_description">"Loading message…"</string>