Merge branch 'develop' into renovate/kotlin
This commit is contained in:
commit
71158bb0c2
363 changed files with 3030 additions and 2267 deletions
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.api
|
||||
|
||||
interface LastMessageTimestampFormatter {
|
||||
fun interface LastMessageTimestampFormatter {
|
||||
fun format(timestamp: Long?): String
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -24,4 +24,9 @@ value class UserId(val value: String) : Serializable {
|
|||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
|
||||
val extractedDisplayName: String
|
||||
get() = value
|
||||
.removePrefix("@")
|
||||
.substringBefore(":")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,5 +10,6 @@ package io.element.android.libraries.matrix.api.room
|
|||
enum class CurrentUserMembership {
|
||||
INVITED,
|
||||
JOINED,
|
||||
LEFT
|
||||
LEFT,
|
||||
KNOCKED,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 d’utilisateur"</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">"L’identité de %1$s semble avoir changé. %2$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new">"L’identité 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 l’utilisateur"</string>
|
||||
<string name="screen_room_member_details_verify_button_subtitle">"Utilisez l’application 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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$s’s %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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue