Merge branch 'develop' into julioromano/poll_history_entry_point

This commit is contained in:
ganfra 2023-12-13 17:22:55 +01:00
commit 863d156e4d
738 changed files with 9387 additions and 1581 deletions

View file

@ -1,4 +1,3 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
@ -33,6 +32,7 @@ dependencies {
implementation(projects.libraries.di)
implementation(projects.libraries.core)
implementation(projects.services.toolbox.api)
implementation(libs.dagger)
implementation(libs.timber)
implementation(libs.androidx.corektx)
@ -41,4 +41,12 @@ dependencies {
implementation(libs.androidx.exifinterface)
implementation(libs.androidx.security.crypto)
api(libs.androidx.browser)
testImplementation(projects.tests.testutils)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.robolectric)
testImplementation(libs.coroutines.core)
testImplementation(libs.coroutines.test)
testImplementation(projects.services.toolbox.test)
}

View file

@ -22,16 +22,18 @@ import android.text.format.Formatter
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class AndroidFileSizeFormatter @Inject constructor(
@ApplicationContext private val context: Context,
) : FileSizeFormatter {
private val sdkIntProvider: BuildVersionSdkIntProvider,
) : FileSizeFormatter {
override fun format(fileSize: Long, useShortFormat: Boolean): String {
// Since Android O, the system considers that 1kB = 1000 bytes instead of 1024 bytes.
// We want to avoid that.
val normalizedSize = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
val normalizedSize = if (sdkIntProvider.get() <= Build.VERSION_CODES.N) {
fileSize
} else {
// First convert the size

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="error_no_compatible_app_found">"Nem található kompatibilis alkalmazás a művelet kezeléséhez."</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="error_no_compatible_app_found">"Tidak ada aplikasi yang kompatibel yang ditemukan untuk menangani tindakan ini."</string>
</resources>

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.androidutils.filesize
import android.os.Build
import com.google.common.truth.Truth.assertThat
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class AndroidFileSizeFormatterTest {
@Test
fun `test api 24 long format`() {
val sut = createAndroidFileSizeFormatter(sdkLevel = Build.VERSION_CODES.N)
assertThat(sut.format(1, useShortFormat = false)).isEqualTo("1.00B")
assertThat(sut.format(1000, useShortFormat = false)).isEqualTo("0.98KB")
assertThat(sut.format(1024, useShortFormat = false)).isEqualTo("1.00KB")
assertThat(sut.format(1024 * 1024, useShortFormat = false)).isEqualTo("1.00MB")
assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = false)).isEqualTo("1.00GB")
}
@Test
fun `test api 26 long format`() {
val sut = createAndroidFileSizeFormatter(sdkLevel = Build.VERSION_CODES.O)
assertThat(sut.format(1, useShortFormat = false)).isEqualTo("1.00B")
assertThat(sut.format(1000, useShortFormat = false)).isEqualTo("0.98KB")
assertThat(sut.format(1024 * 1024, useShortFormat = false)).isEqualTo("0.95MB")
assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = false)).isEqualTo("0.93GB")
}
@Test
fun `test api 24 short format`() {
val sut = createAndroidFileSizeFormatter(sdkLevel = Build.VERSION_CODES.N)
assertThat(sut.format(1, useShortFormat = true)).isEqualTo("1.0B")
assertThat(sut.format(1000, useShortFormat = true)).isEqualTo("0.98KB")
assertThat(sut.format(1024, useShortFormat = true)).isEqualTo("1.0KB")
assertThat(sut.format(1024 * 1024, useShortFormat = true)).isEqualTo("1.0MB")
assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = true)).isEqualTo("1.0GB")
}
@Test
fun `test api 26 short format`() {
val sut = createAndroidFileSizeFormatter(sdkLevel = Build.VERSION_CODES.O)
assertThat(sut.format(1, useShortFormat = true)).isEqualTo("1.0B")
assertThat(sut.format(1000, useShortFormat = true)).isEqualTo("0.98KB")
assertThat(sut.format(1024 * 1024, useShortFormat = true)).isEqualTo("0.95MB")
assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = true)).isEqualTo("0.93GB")
}
private fun createAndroidFileSizeFormatter(sdkLevel: Int) = AndroidFileSizeFormatter(
context = RuntimeEnvironment.getApplication(),
sdkIntProvider = FakeBuildVersionSdkIntProvider(sdkInt = sdkLevel)
)
}

View file

@ -25,7 +25,6 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
@ -58,14 +57,13 @@ import javax.inject.Inject
@ContributesBinding(SessionScope::class)
class DefaultRoomLastMessageFormatter @Inject constructor(
private val sp: StringProvider,
private val matrixClient: MatrixClient,
private val roomMembershipContentFormatter: RoomMembershipContentFormatter,
private val profileChangeContentFormatter: ProfileChangeContentFormatter,
private val stateContentFormatter: StateContentFormatter,
) : RoomLastMessageFormatter {
override fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? {
val isOutgoing = matrixClient.isMe(event.sender)
val isOutgoing = event.isOwn
val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value
return when (val content = event.content) {
is MessageContent -> processMessageContents(content, senderDisplayName, isDmRoom)

View file

@ -21,7 +21,6 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
@ -42,7 +41,6 @@ import javax.inject.Inject
@ContributesBinding(SessionScope::class)
class DefaultTimelineEventFormatter @Inject constructor(
private val sp: StringProvider,
private val matrixClient: MatrixClient,
private val buildMeta: BuildMeta,
private val roomMembershipContentFormatter: RoomMembershipContentFormatter,
private val profileChangeContentFormatter: ProfileChangeContentFormatter,
@ -50,7 +48,7 @@ class DefaultTimelineEventFormatter @Inject constructor(
) : TimelineEventFormatter {
override fun format(event: EventTimelineItem): CharSequence? {
val isOutgoing = matrixClient.isMe(event.sender)
val isOutgoing = event.isOwn
val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value
return when (val content = event.content) {
is RoomMembershipContent -> {

View file

@ -0,0 +1,57 @@
<?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">"(a profilkép is megváltozott)"</string>
<string name="state_event_avatar_url_changed">"%1$s megváltoztatta a profilképét"</string>
<string name="state_event_avatar_url_changed_by_you">"Megváltoztatta a profilképét"</string>
<string name="state_event_display_name_changed_from">"%1$s megváltoztatta a megjelenítendő nevét: %2$s → %3$s"</string>
<string name="state_event_display_name_changed_from_by_you">"Megváltoztatta a megjelenítendő nevét: %1$s → %2$s"</string>
<string name="state_event_display_name_removed">"%1$s eltávolította a megjelenítendő nevét (ez volt: %2$s)"</string>
<string name="state_event_display_name_removed_by_you">"Eltávolította a megjelenítendő nevét (ez volt: %1$s)"</string>
<string name="state_event_display_name_set">"%1$s beállította a megjelenítendő nevét: %2$s"</string>
<string name="state_event_display_name_set_by_you">"Beállította a megjelenítendő nevét: %1$s"</string>
<string name="state_event_room_avatar_changed">"%1$s megváltoztatta a szoba profilképét"</string>
<string name="state_event_room_avatar_changed_by_you">"Megváltoztatta a szoba profilképét"</string>
<string name="state_event_room_avatar_removed">"%1$s eltávolította a szoba profilképét"</string>
<string name="state_event_room_avatar_removed_by_you">"Eltávolítottad a szoba profilképét"</string>
<string name="state_event_room_ban">"%1$s kitiltotta: %2$s"</string>
<string name="state_event_room_ban_by_you">"Kitiltotta: %1$s"</string>
<string name="state_event_room_created">"%1$s létrehozta a szobát"</string>
<string name="state_event_room_created_by_you">"Létrehozta a szobát"</string>
<string name="state_event_room_invite">"%1$s meghívta: %2$s"</string>
<string name="state_event_room_invite_accepted">"%1$s elfogadta a meghívást"</string>
<string name="state_event_room_invite_accepted_by_you">"Elfogadta a meghívást"</string>
<string name="state_event_room_invite_by_you">"Meghívta: %1$s"</string>
<string name="state_event_room_invite_you">"%1$s meghívta"</string>
<string name="state_event_room_join">"%1$s csatlakozott a szobához"</string>
<string name="state_event_room_join_by_you">"Csatlakozott a szobához"</string>
<string name="state_event_room_knock">"%1$s kérte, hogy csatlakozhasson"</string>
<string name="state_event_room_knock_accepted">"%1$s engedélyezte, hogy %2$s csatlakozhasson"</string>
<string name="state_event_room_knock_accepted_by_you">"Engedélyezte, hogy %1$s csatlakozhasson"</string>
<string name="state_event_room_knock_by_you">"Kérte, hogy csatlakozhasson"</string>
<string name="state_event_room_knock_denied">"%1$s elutasította %2$s kérését, hogy csatlakozhasson"</string>
<string name="state_event_room_knock_denied_by_you">"Elutasította %1$s kérését, hogy csatlakozhasson"</string>
<string name="state_event_room_knock_denied_you">"%1$s elutasította a kérését, hogy csatlakozhasson"</string>
<string name="state_event_room_knock_retracted">"%1$s már nem akar csatlakozni"</string>
<string name="state_event_room_knock_retracted_by_you">"Lemondta a csatlakozási kérését"</string>
<string name="state_event_room_leave">"%1$s elhagyta a szobát"</string>
<string name="state_event_room_leave_by_you">"Elhagyta a szobát"</string>
<string name="state_event_room_name_changed">"%1$s megváltoztatta a szoba nevét: %2$s"</string>
<string name="state_event_room_name_changed_by_you">"Megváltoztatta a szoba nevét: %1$s"</string>
<string name="state_event_room_name_removed">"%1$s eltávolította a szoba nevét"</string>
<string name="state_event_room_name_removed_by_you">"Eltávolította a szoba nevét"</string>
<string name="state_event_room_reject">"%1$s elutasította a meghívást"</string>
<string name="state_event_room_reject_by_you">"Elutasította a meghívást"</string>
<string name="state_event_room_remove">"%1$s eltávolította: %2$s"</string>
<string name="state_event_room_remove_by_you">"Eltávolította: %1$s"</string>
<string name="state_event_room_third_party_invite">"%1$s meghívót küldött %2$s számára, hogy csatlakozzon a szobához"</string>
<string name="state_event_room_third_party_invite_by_you">"Meghívót küldött %1$s számára, hogy csatlakozzon a szobához"</string>
<string name="state_event_room_third_party_revoked_invite">"%1$s visszavonta %2$s meghívását, hogy csatlakozzon a szobához"</string>
<string name="state_event_room_third_party_revoked_invite_by_you">"Visszavonta %1$s meghívását, hogy csatlakozzon a szobához"</string>
<string name="state_event_room_topic_changed">"%1$s megváltoztatta a témát: %2$s"</string>
<string name="state_event_room_topic_changed_by_you">"Megváltoztatta a témát: %1$s"</string>
<string name="state_event_room_topic_removed">"%1$s eltávolította a szoba témáját"</string>
<string name="state_event_room_topic_removed_by_you">"Eltávolította a szoba témáját"</string>
<string name="state_event_room_unban">"%1$s visszavonta %2$s kitiltását"</string>
<string name="state_event_room_unban_by_you">"Visszavonta %1$s kitiltását"</string>
<string name="state_event_room_unknown_membership_change">"%1$s ismeretlen változást hajtott végre a tagságában"</string>
</resources>

View file

@ -0,0 +1,57 @@
<?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">"(avatar juga diubah)"</string>
<string name="state_event_avatar_url_changed">"%1$s mengubah avatarnya"</string>
<string name="state_event_avatar_url_changed_by_you">"Anda mengubah avatar sendiri"</string>
<string name="state_event_display_name_changed_from">"%1$s mengubah nama tampilannya dari %2$s menjadi %3$s"</string>
<string name="state_event_display_name_changed_from_by_you">"Anda mengubah nama tampilan sendiri dari %1$s menjadi %2$s"</string>
<string name="state_event_display_name_removed">"%1$s menghapus nama tampilannya (sebelumnya %2$s)"</string>
<string name="state_event_display_name_removed_by_you">"Anda menghapus nama tampilan sendiri (sebelumnya %1$s)"</string>
<string name="state_event_display_name_set">"%1$s menetapkan nama tampilannya menjadi %2$s"</string>
<string name="state_event_display_name_set_by_you">"Anda menetapkan nama tampilan sendiri menjadi %1$s"</string>
<string name="state_event_room_avatar_changed">"%1$s mengubah avatar ruangan"</string>
<string name="state_event_room_avatar_changed_by_you">"Anda mengubah avatar ruangan"</string>
<string name="state_event_room_avatar_removed">"%1$s menghapus avatar ruangan"</string>
<string name="state_event_room_avatar_removed_by_you">"Anda menghapus avatar ruangan"</string>
<string name="state_event_room_ban">"%1$s memblokir %2$s"</string>
<string name="state_event_room_ban_by_you">"Anda memblokir %1$s"</string>
<string name="state_event_room_created">"%1$s membuat ruangan"</string>
<string name="state_event_room_created_by_you">"Anda membuat ruangan"</string>
<string name="state_event_room_invite">"%1$s mengundang %2$s"</string>
<string name="state_event_room_invite_accepted">"%1$s menerima undangan"</string>
<string name="state_event_room_invite_accepted_by_you">"Anda menerima undangan"</string>
<string name="state_event_room_invite_by_you">"Anda mengundang %1$s"</string>
<string name="state_event_room_invite_you">"%1$s mengundang Anda"</string>
<string name="state_event_room_join">"%1$s bergabung ke ruangan"</string>
<string name="state_event_room_join_by_you">"Anda bergabung ke ruangan"</string>
<string name="state_event_room_knock">"%1$s meminta untuk bergabung"</string>
<string name="state_event_room_knock_accepted">"%1$s memperbolehkan %2$s untuk bergabung"</string>
<string name="state_event_room_knock_accepted_by_you">"%1$s memperbolehkan Anda untuk bergabung"</string>
<string name="state_event_room_knock_by_you">"Anda meminta untuk bergabung"</string>
<string name="state_event_room_knock_denied">"%1$s menolak permintaan %2$s untuk bergabung"</string>
<string name="state_event_room_knock_denied_by_you">"Anda menolak permintaan %1$s untuk bergabung"</string>
<string name="state_event_room_knock_denied_you">"%1$s menolak permintaan Anda untuk bergabung"</string>
<string name="state_event_room_knock_retracted">"%1$s tidak lagi tertarik untuk bergabung"</string>
<string name="state_event_room_knock_retracted_by_you">"Anda membatalkan permintaan sendiri untuk bergabung"</string>
<string name="state_event_room_leave">"%1$s meninggalkan ruangan"</string>
<string name="state_event_room_leave_by_you">"Anda keluar dari ruangan"</string>
<string name="state_event_room_name_changed">"%1$s mengubah nama ruangan menjadi: %2$s"</string>
<string name="state_event_room_name_changed_by_you">"Anda mengubah nama ruangan menjadi: %1$s"</string>
<string name="state_event_room_name_removed">"%1$s menghapus nama ruangan"</string>
<string name="state_event_room_name_removed_by_you">"Anda menghapus nama ruangan"</string>
<string name="state_event_room_reject">"%1$s menolak undangan"</string>
<string name="state_event_room_reject_by_you">"Anda menolak undangan"</string>
<string name="state_event_room_remove">"%1$s mengeluarkan %2$s"</string>
<string name="state_event_room_remove_by_you">"Anda mengeluarkan %1$s"</string>
<string name="state_event_room_third_party_invite">"%1$s mengirimkan undangan kepada %2$s untuk bergabung ke ruangan"</string>
<string name="state_event_room_third_party_invite_by_you">"Anda mengirimkan undangan kepada %1$s untuk bergabung ke ruangan"</string>
<string name="state_event_room_third_party_revoked_invite">"%1$s menghapus undangan kepada %2$s untuk bergabung ke ruangan"</string>
<string name="state_event_room_third_party_revoked_invite_by_you">"Anda menghapus undangan kepada %1$s untuk bergabung ke ruangan"</string>
<string name="state_event_room_topic_changed">"%1$s mengubah topik menjadi: %2$s"</string>
<string name="state_event_room_topic_changed_by_you">"Anda mengubah topik menjadi: %1$s"</string>
<string name="state_event_room_topic_removed">"%1$s menghapus topik ruangan"</string>
<string name="state_event_room_topic_removed_by_you">"Anda menghapus topik ruangan"</string>
<string name="state_event_room_unban">"%1$s membatalkan pemblokiran %2$s"</string>
<string name="state_event_room_unban_by_you">"Anda membatalkan pemblokiran %1$s"</string>
<string name="state_event_room_unknown_membership_change">"%1$s membuat perubahan keanggotaan yang tidak diketahui"</string>
</resources>

View file

@ -18,7 +18,8 @@ package io.element.android.libraries.eventformatter.impl
import android.content.Context
import androidx.compose.ui.text.AnnotatedString
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
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
@ -75,7 +76,6 @@ class DefaultRoomLastMessageFormatterTest {
val stringProvider = AndroidStringProvider(context.resources)
formatter = DefaultRoomLastMessageFormatter(
sp = AndroidStringProvider(context.resources),
matrixClient = fakeMatrixClient,
roomMembershipContentFormatter = RoomMembershipContentFormatter(fakeMatrixClient, stringProvider),
profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider),
stateContentFormatter = StateContentFormatter(stringProvider)
@ -91,10 +91,10 @@ class DefaultRoomLastMessageFormatterTest {
val message = createRoomEvent(false, senderName, RedactedContent)
val result = formatter.format(message, isDm)
if (isDm) {
Truth.assertThat(result).isEqualTo(expected)
assertThat(result).isEqualTo(expected)
} else {
Truth.assertThat(result).isInstanceOf(AnnotatedString::class.java)
Truth.assertThat(result.toString()).isEqualTo("$senderName: $expected")
assertThat(result).isInstanceOf(AnnotatedString::class.java)
assertThat(result.toString()).isEqualTo("$senderName: $expected")
}
}
}
@ -106,7 +106,7 @@ class DefaultRoomLastMessageFormatterTest {
val info = ImageInfo(null, null, null, null, null, null, null)
val message = createRoomEvent(false, null, StickerContent(body, info, "url"))
val result = formatter.format(message, false)
Truth.assertThat(result).isEqualTo(body)
assertThat(result).isEqualTo(body)
}
@Test
@ -118,10 +118,10 @@ class DefaultRoomLastMessageFormatterTest {
val message = createRoomEvent(false, senderName, UnableToDecryptContent(UnableToDecryptContent.Data.Unknown))
val result = formatter.format(message, isDm)
if (isDm) {
Truth.assertThat(result).isEqualTo(expected)
assertThat(result).isEqualTo(expected)
} else {
Truth.assertThat(result).isInstanceOf(AnnotatedString::class.java)
Truth.assertThat(result.toString()).isEqualTo("$senderName: $expected")
assertThat(result).isInstanceOf(AnnotatedString::class.java)
assertThat(result.toString()).isEqualTo("$senderName: $expected")
}
}
}
@ -140,10 +140,10 @@ class DefaultRoomLastMessageFormatterTest {
val message = createRoomEvent(false, senderName, type)
val result = formatter.format(message, isDm)
if (isDm) {
Truth.assertWithMessage("$type was not properly handled").that(result).isEqualTo(expected)
assertWithMessage("$type was not properly handled").that(result).isEqualTo(expected)
} else {
Truth.assertWithMessage("$type does not create an AnnotatedString").that(result).isInstanceOf(AnnotatedString::class.java)
Truth.assertWithMessage("$type was not properly handled").that(result.toString()).isEqualTo("$senderName: $expected")
assertWithMessage("$type does not create an AnnotatedString").that(result).isInstanceOf(AnnotatedString::class.java)
assertWithMessage("$type was not properly handled").that(result.toString()).isEqualTo("$senderName: $expected")
}
}
}
@ -203,7 +203,7 @@ class DefaultRoomLastMessageFormatterTest {
is NoticeMessageType,
is OtherMessageType -> body
}
Truth.assertWithMessage("$type was not properly handled for DM").that(result).isEqualTo(expectedResult)
assertWithMessage("$type was not properly handled for DM").that(result).isEqualTo(expectedResult)
}
// Verify results of Room mode
@ -233,11 +233,11 @@ class DefaultRoomLastMessageFormatterTest {
is OtherMessageType -> true
}
if (shouldCreateAnnotatedString) {
Truth.assertWithMessage("$type doesn't produce an AnnotatedString")
assertWithMessage("$type doesn't produce an AnnotatedString")
.that(result)
.isInstanceOf(AnnotatedString::class.java)
}
Truth.assertWithMessage("$type was not properly handled for room").that(string).isEqualTo(expectedResult)
assertWithMessage("$type was not properly handled for room").that(string).isEqualTo(expectedResult)
}
}
@ -254,11 +254,11 @@ class DefaultRoomLastMessageFormatterTest {
val youJoinedRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youJoinedRoom = formatter.format(youJoinedRoomEvent, false)
Truth.assertThat(youJoinedRoom).isEqualTo("You joined the room")
assertThat(youJoinedRoom).isEqualTo("You joined the room")
val someoneJoinedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneJoinedRoom = formatter.format(someoneJoinedRoomEvent, false)
Truth.assertThat(someoneJoinedRoom).isEqualTo("${someoneContent.userId} joined the room")
assertThat(someoneJoinedRoom).isEqualTo("${someoneContent.userId} joined the room")
}
@Test
@ -270,11 +270,11 @@ class DefaultRoomLastMessageFormatterTest {
val youLeftRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youLeftRoom = formatter.format(youLeftRoomEvent, false)
Truth.assertThat(youLeftRoom).isEqualTo("You left the room")
assertThat(youLeftRoom).isEqualTo("You left the room")
val someoneLeftRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneLeftRoom = formatter.format(someoneLeftRoomEvent, false)
Truth.assertThat(someoneLeftRoom).isEqualTo("${someoneContent.userId} left the room")
assertThat(someoneLeftRoom).isEqualTo("${someoneContent.userId} left the room")
}
@Test
@ -288,19 +288,19 @@ class DefaultRoomLastMessageFormatterTest {
val youBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youBanned = formatter.format(youBannedEvent, false)
Truth.assertThat(youBanned).isEqualTo("You banned ${youContent.userId}")
assertThat(youBanned).isEqualTo("You banned ${youContent.userId}")
val youKickBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youKickedContent)
val youKickedBanned = formatter.format(youKickBannedEvent, false)
Truth.assertThat(youKickedBanned).isEqualTo("You banned ${youContent.userId}")
assertThat(youKickedBanned).isEqualTo("You banned ${youContent.userId}")
val someoneBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneBanned = formatter.format(someoneBannedEvent, false)
Truth.assertThat(someoneBanned).isEqualTo("$otherName banned ${someoneContent.userId}")
assertThat(someoneBanned).isEqualTo("$otherName banned ${someoneContent.userId}")
val someoneKickBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneKickedContent)
val someoneKickBanned = formatter.format(someoneKickBannedEvent, false)
Truth.assertThat(someoneKickBanned).isEqualTo("$otherName banned ${someoneContent.userId}")
assertThat(someoneKickBanned).isEqualTo("$otherName banned ${someoneContent.userId}")
}
@Test
@ -312,11 +312,11 @@ class DefaultRoomLastMessageFormatterTest {
val youUnbannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youUnbanned = formatter.format(youUnbannedEvent, false)
Truth.assertThat(youUnbanned).isEqualTo("You unbanned ${youContent.userId}")
assertThat(youUnbanned).isEqualTo("You unbanned ${youContent.userId}")
val someoneUnbannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneUnbanned = formatter.format(someoneUnbannedEvent, false)
Truth.assertThat(someoneUnbanned).isEqualTo("$otherName unbanned ${someoneContent.userId}")
assertThat(someoneUnbanned).isEqualTo("$otherName unbanned ${someoneContent.userId}")
}
@Test
@ -328,11 +328,11 @@ class DefaultRoomLastMessageFormatterTest {
val youKickedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youKicked = formatter.format(youKickedEvent, false)
Truth.assertThat(youKicked).isEqualTo("You removed ${youContent.userId}")
assertThat(youKicked).isEqualTo("You removed ${youContent.userId}")
val someoneKickedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneKicked = formatter.format(someoneKickedEvent, false)
Truth.assertThat(someoneKicked).isEqualTo("$otherName removed ${someoneContent.userId}")
assertThat(someoneKicked).isEqualTo("$otherName removed ${someoneContent.userId}")
}
@Test
@ -344,15 +344,15 @@ class DefaultRoomLastMessageFormatterTest {
val youWereInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent)
val youWereInvited = formatter.format(youWereInvitedEvent, false)
Truth.assertThat(youWereInvited).isEqualTo("$otherName invited you")
assertThat(youWereInvited).isEqualTo("$otherName invited you")
val youInvitedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youInvited = formatter.format(youInvitedEvent, false)
Truth.assertThat(youInvited).isEqualTo("You invited ${someoneContent.userId}")
assertThat(youInvited).isEqualTo("You invited ${someoneContent.userId}")
val someoneInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneInvited = formatter.format(someoneInvitedEvent, false)
Truth.assertThat(someoneInvited).isEqualTo("$otherName invited ${someoneContent.userId}")
assertThat(someoneInvited).isEqualTo("$otherName invited ${someoneContent.userId}")
}
@Test
@ -364,11 +364,11 @@ class DefaultRoomLastMessageFormatterTest {
val youAcceptedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youAcceptedInvite = formatter.format(youAcceptedInviteEvent, false)
Truth.assertThat(youAcceptedInvite).isEqualTo("You accepted the invite")
assertThat(youAcceptedInvite).isEqualTo("You accepted the invite")
val someoneAcceptedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneAcceptedInvite = formatter.format(someoneAcceptedInviteEvent, false)
Truth.assertThat(someoneAcceptedInvite).isEqualTo("${someoneContent.userId} accepted the invite")
assertThat(someoneAcceptedInvite).isEqualTo("${someoneContent.userId} accepted the invite")
}
@Test
@ -380,11 +380,11 @@ class DefaultRoomLastMessageFormatterTest {
val youRejectedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youRejectedInvite = formatter.format(youRejectedInviteEvent, false)
Truth.assertThat(youRejectedInvite).isEqualTo("You rejected the invitation")
assertThat(youRejectedInvite).isEqualTo("You rejected the invitation")
val someoneRejectedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneRejectedInvite = formatter.format(someoneRejectedInviteEvent, false)
Truth.assertThat(someoneRejectedInvite).isEqualTo("${someoneContent.userId} rejected the invitation")
assertThat(someoneRejectedInvite).isEqualTo("${someoneContent.userId} rejected the invitation")
}
@Test
@ -395,11 +395,11 @@ class DefaultRoomLastMessageFormatterTest {
val youRevokedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youRevokedInvite = formatter.format(youRevokedInviteEvent, false)
Truth.assertThat(youRevokedInvite).isEqualTo("You revoked the invitation for ${someoneContent.userId} to join the room")
assertThat(youRevokedInvite).isEqualTo("You revoked the invitation for ${someoneContent.userId} to join the room")
val someoneRevokedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneRevokedInvite = formatter.format(someoneRevokedInviteEvent, false)
Truth.assertThat(someoneRevokedInvite).isEqualTo("$otherName revoked the invitation for ${someoneContent.userId} to join the room")
assertThat(someoneRevokedInvite).isEqualTo("$otherName revoked the invitation for ${someoneContent.userId} to join the room")
}
@Test
@ -411,11 +411,11 @@ class DefaultRoomLastMessageFormatterTest {
val youKnockedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youKnocked = formatter.format(youKnockedEvent, false)
Truth.assertThat(youKnocked).isEqualTo("You requested to join")
assertThat(youKnocked).isEqualTo("You requested to join")
val someoneKnockedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneKnocked = formatter.format(someoneKnockedEvent, false)
Truth.assertThat(someoneKnocked).isEqualTo("${someoneContent.userId} requested to join")
assertThat(someoneKnocked).isEqualTo("${someoneContent.userId} requested to join")
}
@Test
@ -426,11 +426,11 @@ class DefaultRoomLastMessageFormatterTest {
val youAcceptedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youAcceptedKnock = formatter.format(youAcceptedKnockEvent, false)
Truth.assertThat(youAcceptedKnock).isEqualTo("${someoneContent.userId} allowed you to join")
assertThat(youAcceptedKnock).isEqualTo("${someoneContent.userId} allowed you to join")
val someoneAcceptedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneAcceptedKnock = formatter.format(someoneAcceptedKnockEvent, false)
Truth.assertThat(someoneAcceptedKnock).isEqualTo("$otherName allowed ${someoneContent.userId} to join")
assertThat(someoneAcceptedKnock).isEqualTo("$otherName allowed ${someoneContent.userId} to join")
}
@Test
@ -442,11 +442,11 @@ class DefaultRoomLastMessageFormatterTest {
val youRetractedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youRetractedKnock = formatter.format(youRetractedKnockEvent, false)
Truth.assertThat(youRetractedKnock).isEqualTo("You cancelled your request to join")
assertThat(youRetractedKnock).isEqualTo("You cancelled your request to join")
val someoneRetractedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneRetractedKnock = formatter.format(someoneRetractedKnockEvent, false)
Truth.assertThat(someoneRetractedKnock).isEqualTo("${someoneContent.userId} is no longer interested in joining")
assertThat(someoneRetractedKnock).isEqualTo("${someoneContent.userId} is no longer interested in joining")
}
@Test
@ -458,15 +458,15 @@ class DefaultRoomLastMessageFormatterTest {
val youDeniedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youDeniedKnock = formatter.format(youDeniedKnockEvent, false)
Truth.assertThat(youDeniedKnock).isEqualTo("You rejected ${someoneContent.userId}'s request to join")
assertThat(youDeniedKnock).isEqualTo("You rejected ${someoneContent.userId}'s request to join")
val someoneDeniedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneDeniedKnock = formatter.format(someoneDeniedKnockEvent, false)
Truth.assertThat(someoneDeniedKnock).isEqualTo("$otherName rejected ${someoneContent.userId}'s request to join")
assertThat(someoneDeniedKnock).isEqualTo("$otherName rejected ${someoneContent.userId}'s request to join")
val someoneDeniedYourKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent)
val someoneDeniedYourKnock = formatter.format(someoneDeniedYourKnockEvent, false)
Truth.assertThat(someoneDeniedYourKnock).isEqualTo("$otherName rejected your request to join")
assertThat(someoneDeniedYourKnock).isEqualTo("$otherName rejected your request to join")
}
@Test
@ -481,7 +481,7 @@ class DefaultRoomLastMessageFormatterTest {
change to result
}
val expected = otherChanges.map { it to null }
Truth.assertThat(results).isEqualTo(expected)
assertThat(results).isEqualTo(expected)
}
// endregion
@ -497,19 +497,19 @@ class DefaultRoomLastMessageFormatterTest {
val youChangedRoomAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
val youChangedRoomAvatar = formatter.format(youChangedRoomAvatarEvent, false)
Truth.assertThat(youChangedRoomAvatar).isEqualTo("You changed the room avatar")
assertThat(youChangedRoomAvatar).isEqualTo("You changed the room avatar")
val someoneChangedRoomAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent)
val someoneChangedRoomAvatar = formatter.format(someoneChangedRoomAvatarEvent, false)
Truth.assertThat(someoneChangedRoomAvatar).isEqualTo("$otherName changed the room avatar")
assertThat(someoneChangedRoomAvatar).isEqualTo("$otherName changed the room avatar")
val youRemovedRoomAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent)
val youRemovedRoomAvatar = formatter.format(youRemovedRoomAvatarEvent, false)
Truth.assertThat(youRemovedRoomAvatar).isEqualTo("You removed the room avatar")
assertThat(youRemovedRoomAvatar).isEqualTo("You removed the room avatar")
val someoneRemovedRoomAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent)
val someoneRemovedRoomAvatar = formatter.format(someoneRemovedRoomAvatarEvent, false)
Truth.assertThat(someoneRemovedRoomAvatar).isEqualTo("$otherName removed the room avatar")
assertThat(someoneRemovedRoomAvatar).isEqualTo("$otherName removed the room avatar")
}
@Test
@ -520,11 +520,11 @@ class DefaultRoomLastMessageFormatterTest {
val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content)
val youCreatedRoom = formatter.format(youCreatedRoomMessage, false)
Truth.assertThat(youCreatedRoom).isEqualTo("You created the room")
assertThat(youCreatedRoom).isEqualTo("You created the room")
val someoneCreatedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = content)
val someoneCreatedRoom = formatter.format(someoneCreatedRoomEvent, false)
Truth.assertThat(someoneCreatedRoom).isEqualTo("$otherName created the room")
assertThat(someoneCreatedRoom).isEqualTo("$otherName created the room")
}
@Test
@ -535,11 +535,11 @@ class DefaultRoomLastMessageFormatterTest {
val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content)
val youCreatedRoom = formatter.format(youCreatedRoomMessage, false)
Truth.assertThat(youCreatedRoom).isEqualTo("Encryption enabled")
assertThat(youCreatedRoom).isEqualTo("Encryption enabled")
val someoneCreatedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = content)
val someoneCreatedRoom = formatter.format(someoneCreatedRoomEvent, false)
Truth.assertThat(someoneCreatedRoom).isEqualTo("Encryption enabled")
assertThat(someoneCreatedRoom).isEqualTo("Encryption enabled")
}
@Test
@ -552,19 +552,19 @@ class DefaultRoomLastMessageFormatterTest {
val youChangedRoomNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
val youChangedRoomName = formatter.format(youChangedRoomNameEvent, false)
Truth.assertThat(youChangedRoomName).isEqualTo("You changed the room name to: $newName")
assertThat(youChangedRoomName).isEqualTo("You changed the room name to: $newName")
val someoneChangedRoomNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent)
val someoneChangedRoomName = formatter.format(someoneChangedRoomNameEvent, false)
Truth.assertThat(someoneChangedRoomName).isEqualTo("$otherName changed the room name to: $newName")
assertThat(someoneChangedRoomName).isEqualTo("$otherName changed the room name to: $newName")
val youRemovedRoomNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent)
val youRemovedRoomName = formatter.format(youRemovedRoomNameEvent, false)
Truth.assertThat(youRemovedRoomName).isEqualTo("You removed the room name")
assertThat(youRemovedRoomName).isEqualTo("You removed the room name")
val someoneRemovedRoomNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent)
val someoneRemovedRoomName = formatter.format(someoneRemovedRoomNameEvent, false)
Truth.assertThat(someoneRemovedRoomName).isEqualTo("$otherName removed the room name")
assertThat(someoneRemovedRoomName).isEqualTo("$otherName removed the room name")
}
@Test
@ -577,19 +577,19 @@ class DefaultRoomLastMessageFormatterTest {
val youInvitedSomeoneEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
val youInvitedSomeone = formatter.format(youInvitedSomeoneEvent, false)
Truth.assertThat(youInvitedSomeone).isEqualTo("You sent an invitation to $inviteeName to join the room")
assertThat(youInvitedSomeone).isEqualTo("You sent an invitation to $inviteeName to join the room")
val someoneInvitedSomeoneEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent)
val someoneInvitedSomeone = formatter.format(someoneInvitedSomeoneEvent, false)
Truth.assertThat(someoneInvitedSomeone).isEqualTo("$otherName sent an invitation to $inviteeName to join the room")
assertThat(someoneInvitedSomeone).isEqualTo("$otherName sent an invitation to $inviteeName to join the room")
val youInvitedNoOneEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent)
val youInvitedNoOne = formatter.format(youInvitedNoOneEvent, false)
Truth.assertThat(youInvitedNoOne).isNull()
assertThat(youInvitedNoOne).isNull()
val someoneInvitedNoOneEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent)
val someoneInvitedNoOne = formatter.format(someoneInvitedNoOneEvent, false)
Truth.assertThat(someoneInvitedNoOne).isNull()
assertThat(someoneInvitedNoOne).isNull()
}
@Test
@ -602,19 +602,19 @@ class DefaultRoomLastMessageFormatterTest {
val youChangedRoomTopicEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
val youChangedRoomTopic = formatter.format(youChangedRoomTopicEvent, false)
Truth.assertThat(youChangedRoomTopic).isEqualTo("You changed the topic to: $roomTopic")
assertThat(youChangedRoomTopic).isEqualTo("You changed the topic to: $roomTopic")
val someoneChangedRoomTopicEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent)
val someoneChangedRoomTopic = formatter.format(someoneChangedRoomTopicEvent, false)
Truth.assertThat(someoneChangedRoomTopic).isEqualTo("$otherName changed the topic to: $roomTopic")
assertThat(someoneChangedRoomTopic).isEqualTo("$otherName changed the topic to: $roomTopic")
val youRemovedRoomTopicEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent)
val youRemovedRoomTopic = formatter.format(youRemovedRoomTopicEvent, false)
Truth.assertThat(youRemovedRoomTopic).isEqualTo("You removed the room topic")
assertThat(youRemovedRoomTopic).isEqualTo("You removed the room topic")
val someoneRemovedRoomTopicEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent)
val someoneRemovedRoomTopic = formatter.format(someoneRemovedRoomTopicEvent, false)
Truth.assertThat(someoneRemovedRoomTopic).isEqualTo("$otherName removed the room topic")
assertThat(someoneRemovedRoomTopic).isEqualTo("$otherName removed the room topic")
}
@Test
@ -633,7 +633,7 @@ class DefaultRoomLastMessageFormatterTest {
state to result
}
val expected = otherStates.map { it to null }
Truth.assertThat(results).isEqualTo(expected)
assertThat(results).isEqualTo(expected)
}
// endregion
@ -652,35 +652,35 @@ class DefaultRoomLastMessageFormatterTest {
val youChangedAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
val youChangedAvatar = formatter.format(youChangedAvatarEvent, false)
Truth.assertThat(youChangedAvatar).isEqualTo("You changed your avatar")
assertThat(youChangedAvatar).isEqualTo("You changed your avatar")
val someoneChangeAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent)
val someoneChangeAvatar = formatter.format(someoneChangeAvatarEvent, false)
Truth.assertThat(someoneChangeAvatar).isEqualTo("$otherName changed their avatar")
assertThat(someoneChangeAvatar).isEqualTo("$otherName changed their avatar")
val youSetAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = setContent)
val youSetAvatar = formatter.format(youSetAvatarEvent, false)
Truth.assertThat(youSetAvatar).isEqualTo("You changed your avatar")
assertThat(youSetAvatar).isEqualTo("You changed your avatar")
val someoneSetAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = setContent)
val someoneSetAvatar = formatter.format(someoneSetAvatarEvent, false)
Truth.assertThat(someoneSetAvatar).isEqualTo("$otherName changed their avatar")
assertThat(someoneSetAvatar).isEqualTo("$otherName changed their avatar")
val youRemovedAvatarEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent)
val youRemovedAvatar = formatter.format(youRemovedAvatarEvent, false)
Truth.assertThat(youRemovedAvatar).isEqualTo("You changed your avatar")
assertThat(youRemovedAvatar).isEqualTo("You changed your avatar")
val someoneRemovedAvatarEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent)
val someoneRemovedAvatar = formatter.format(someoneRemovedAvatarEvent, false)
Truth.assertThat(someoneRemovedAvatar).isEqualTo("$otherName changed their avatar")
assertThat(someoneRemovedAvatar).isEqualTo("$otherName changed their avatar")
val unchangedEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = sameContent)
val unchangedResult = formatter.format(unchangedEvent, false)
Truth.assertThat(unchangedResult).isNull()
assertThat(unchangedResult).isNull()
val invalidEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = invalidContent)
val invalidResult = formatter.format(invalidEvent, false)
Truth.assertThat(invalidResult).isNull()
assertThat(invalidResult).isNull()
}
@Test
@ -697,35 +697,35 @@ class DefaultRoomLastMessageFormatterTest {
val youChangedDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
val youChangedDisplayName = formatter.format(youChangedDisplayNameEvent, false)
Truth.assertThat(youChangedDisplayName).isEqualTo("You changed your display name from $oldDisplayName to $newDisplayName")
assertThat(youChangedDisplayName).isEqualTo("You changed your display name from $oldDisplayName to $newDisplayName")
val someoneChangedDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = changedContent)
val someoneChangedDisplayName = formatter.format(someoneChangedDisplayNameEvent, false)
Truth.assertThat(someoneChangedDisplayName).isEqualTo("$otherName changed their display name from $oldDisplayName to $newDisplayName")
assertThat(someoneChangedDisplayName).isEqualTo("$otherName changed their display name from $oldDisplayName to $newDisplayName")
val youSetDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = setContent)
val youSetDisplayName = formatter.format(youSetDisplayNameEvent, false)
Truth.assertThat(youSetDisplayName).isEqualTo("You set your display name to $newDisplayName")
assertThat(youSetDisplayName).isEqualTo("You set your display name to $newDisplayName")
val someoneSetDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = setContent)
val someoneSetDisplayName = formatter.format(someoneSetDisplayNameEvent, false)
Truth.assertThat(someoneSetDisplayName).isEqualTo("$otherName set their display name to $newDisplayName")
assertThat(someoneSetDisplayName).isEqualTo("$otherName set their display name to $newDisplayName")
val youRemovedDisplayNameEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = removedContent)
val youRemovedDisplayName = formatter.format(youRemovedDisplayNameEvent, false)
Truth.assertThat(youRemovedDisplayName).isEqualTo("You removed your display name (it was $oldDisplayName)")
assertThat(youRemovedDisplayName).isEqualTo("You removed your display name (it was $oldDisplayName)")
val someoneRemovedDisplayNameEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = removedContent)
val someoneRemovedDisplayName = formatter.format(someoneRemovedDisplayNameEvent, false)
Truth.assertThat(someoneRemovedDisplayName).isEqualTo("$otherName removed their display name (it was $oldDisplayName)")
assertThat(someoneRemovedDisplayName).isEqualTo("$otherName removed their display name (it was $oldDisplayName)")
val unchangedEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = sameContent)
val unchangedResult = formatter.format(unchangedEvent, false)
Truth.assertThat(unchangedResult).isNull()
assertThat(unchangedResult).isNull()
val invalidEvent = createRoomEvent(sentByYou = true, senderDisplayName = otherName, content = invalidContent)
val invalidResult = formatter.format(invalidEvent, false)
Truth.assertThat(invalidResult).isNull()
assertThat(invalidResult).isNull()
}
@Test
@ -754,15 +754,15 @@ class DefaultRoomLastMessageFormatterTest {
val youChangedBothEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = changedContent)
val youChangedBoth = formatter.format(youChangedBothEvent, false)
Truth.assertThat(youChangedBoth).isEqualTo("You changed your display name from $oldDisplayName to $newDisplayName\n(avatar was changed too)")
assertThat(youChangedBoth).isEqualTo("You changed your display name from $oldDisplayName to $newDisplayName\n(avatar was changed too)")
val invalidContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = invalidContent)
val invalidMessage = formatter.format(invalidContentEvent, false)
Truth.assertThat(invalidMessage).isNull()
assertThat(invalidMessage).isNull()
val sameContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = sameContent)
val sameMessage = formatter.format(sameContentEvent, false)
Truth.assertThat(sameMessage).isNull()
assertThat(sameMessage).isNull()
}
// endregion
@ -775,10 +775,10 @@ class DefaultRoomLastMessageFormatterTest {
val pollContent = aPollContent()
val mineContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = "Alice", content = pollContent)
Truth.assertThat(formatter.format(mineContentEvent, true)).isEqualTo("Poll: Do you like polls?")
assertThat(formatter.format(mineContentEvent, true)).isEqualTo("Poll: Do you like polls?")
val contentEvent = createRoomEvent(sentByYou = false, senderDisplayName = "Bob", content = pollContent)
Truth.assertThat(formatter.format(contentEvent, true)).isEqualTo("Poll: Do you like polls?")
assertThat(formatter.format(contentEvent, true)).isEqualTo("Poll: Do you like polls?")
}
@Test
@ -787,10 +787,10 @@ class DefaultRoomLastMessageFormatterTest {
val pollContent = aPollContent()
val mineContentEvent = createRoomEvent(sentByYou = true, senderDisplayName = "Alice", content = pollContent)
Truth.assertThat(formatter.format(mineContentEvent, false).toString()).isEqualTo("Alice: Poll: Do you like polls?")
assertThat(formatter.format(mineContentEvent, false).toString()).isEqualTo("Alice: Poll: Do you like polls?")
val contentEvent = createRoomEvent(sentByYou = false, senderDisplayName = "Bob", content = pollContent)
Truth.assertThat(formatter.format(contentEvent, false).toString()).isEqualTo("Bob: Poll: Do you like polls?")
assertThat(formatter.format(contentEvent, false).toString()).isEqualTo("Bob: Poll: Do you like polls?")
}
// endregion
@ -798,6 +798,11 @@ class DefaultRoomLastMessageFormatterTest {
private fun createRoomEvent(sentByYou: Boolean, senderDisplayName: String?, content: EventContent): EventTimelineItem {
val sender = if (sentByYou) A_USER_ID else UserId("@someone_else:domain")
val profile = ProfileTimelineDetails.Ready(senderDisplayName, false, null)
return anEventTimelineItem(content = content, senderProfile = profile, sender = sender)
return anEventTimelineItem(
content = content,
senderProfile = profile,
sender = sender,
isOwn = sentByYou,
)
}
}

View file

@ -37,7 +37,7 @@ class DefaultFeatureFlagServiceTest {
fun `given service without provider when set enabled feature is called then it returns false`() = runTest {
val featureFlagService = DefaultFeatureFlagService(emptySet())
val result = featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true)
assertThat(result).isEqualTo(false)
assertThat(result).isFalse()
}
@Test
@ -45,7 +45,7 @@ class DefaultFeatureFlagServiceTest {
val featureFlagProvider = FakeMutableFeatureFlagProvider(0)
val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider))
val result = featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true)
assertThat(result).isEqualTo(true)
assertThat(result).isTrue()
}
@Test
@ -54,9 +54,9 @@ class DefaultFeatureFlagServiceTest {
val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider))
featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true)
featureFlagService.isFeatureEnabledFlow(FeatureFlags.LocationSharing).test {
assertThat(awaitItem()).isEqualTo(true)
assertThat(awaitItem()).isTrue()
featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, false)
assertThat(awaitItem()).isEqualTo(false)
assertThat(awaitItem()).isFalse()
}
}
@ -68,7 +68,7 @@ class DefaultFeatureFlagServiceTest {
lowPriorityFeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, false)
highPriorityFeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, true)
featureFlagService.isFeatureEnabledFlow(FeatureFlags.LocationSharing).test {
assertThat(awaitItem()).isEqualTo(true)
assertThat(awaitItem()).isTrue()
}
}
}

View file

@ -41,7 +41,7 @@ interface MatrixClient : Closeable {
val roomListService: RoomListService
val mediaLoader: MatrixMediaLoader
suspend fun getRoom(roomId: RoomId): MatrixRoom?
suspend fun findDM(userId: UserId): MatrixRoom?
suspend fun findDM(userId: UserId): RoomId?
suspend fun ignoreUser(userId: UserId): Result<Unit>
suspend fun unignoreUser(userId: UserId): Result<Unit>
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>

View file

@ -43,9 +43,9 @@ interface EncryptionService {
suspend fun doesBackupExistOnServer(): Result<Boolean>
/**
* Note: accept bot recoveryKey and passphrase.
* Note: accept both recoveryKey and passphrase.
*/
suspend fun fixRecoveryIssues(recoveryKey: String): Result<Unit>
suspend fun recover(recoveryKey: String): Result<Unit>
/**
* Wait for backup upload steady state.

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.encryption
import io.element.android.libraries.matrix.api.exception.ClientException
sealed class RecoveryException(message: String) : Exception(message) {
class SecretStorage(message: String) : RecoveryException(message)
data object BackupExistsOnServer : RecoveryException("BackupExistsOnServer")
data class Client(val exception: ClientException) : RecoveryException(exception.message ?: "Unknown error")
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.mxc
import javax.inject.Inject
class MxcTools @Inject constructor() {
/**
* Regex to match a Matrix Content (mxc://) URI.
*
* See: https://spec.matrix.org/v1.8/client-server-api/#matrix-content-mxc-uris
*/
private val mxcRegex = Regex("""^mxc://([^/]+)/([^/]+)$""")
/**
* Sanitizes an mxcUri to be used as a relative file path.
*
* @param mxcUri the Matrix Content (mxc://) URI of the file.
* @return the relative file path as "<server-name>/<media-id>" or null if the mxcUri is invalid.
*/
fun mxcUri2FilePath(mxcUri: String): String? = mxcRegex.matchEntire(mxcUri)?.let { match ->
buildString {
append(match.groupValues[1])
append("/")
append(match.groupValues[2])
}
}
}

View file

@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
data class NotificationData(
val eventId: EventId,
val roomId: RoomId,
// mxc url
val senderAvatarUrl: String?,
val senderDisplayName: String?,
val roomAvatarUrl: String?,
@ -34,8 +35,6 @@ data class NotificationData(
val isNoisy: Boolean,
val timestamp: Long,
val content: NotificationContent,
// For images for instance
val contentUrl: String?,
val hasMention: Boolean,
)

View file

@ -38,5 +38,7 @@ interface NotificationSettingsService {
suspend fun setRoomMentionEnabled(enabled: Boolean): Result<Unit>
suspend fun isCallEnabled(): Result<Boolean>
suspend fun setCallEnabled(enabled: Boolean): Result<Unit>
suspend fun isInviteForMeEnabled(): Result<Boolean>
suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit>
suspend fun getRoomsWithUserDefinedRules(): Result<List<String>>
}

View file

@ -132,7 +132,8 @@ interface MatrixRoom : Closeable {
suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean>
suspend fun canUserJoinCall(userId: UserId): Result<Boolean>
suspend fun canUserJoinCall(userId: UserId): Result<Boolean> =
canUserSendState(userId, StateEventType.CALL_MEMBER)
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
/**
* Try to find an existing DM with the given user, or create one if none exists.
*/
suspend fun MatrixClient.startDM(userId: UserId): StartDMResult {
val existingDM = findDM(userId)
return if (existingDM != null) {
StartDMResult.Success(existingDM, isNew = false)
} else {
createDM(userId).fold(
{ StartDMResult.Success(it, isNew = true) },
{ StartDMResult.Failure(it) }
)
}
}
sealed interface StartDMResult {
data class Success(val roomId: RoomId, val isNew: Boolean) : StartDMResult
data class Failure(val throwable: Throwable) : StartDMResult
}

View file

@ -20,6 +20,7 @@ enum class StateEventType {
POLICY_RULE_ROOM,
POLICY_RULE_SERVER,
POLICY_RULE_USER,
CALL_MEMBER,
ROOM_ALIASES,
ROOM_AVATAR,
ROOM_CANONICAL_ALIAS,

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.verification
import androidx.compose.runtime.Immutable
@Immutable
sealed interface SessionVerificationData {
data class Emojis(
// 7 emojis
val emojis: List<VerificationEmoji>,
) : SessionVerificationData
data class Decimals(
// 3 numbers
val decimals: List<Int>,
) : SessionVerificationData
}
// https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji
data class VerificationEmoji(
val number: Int,
val emoji: String,
val description: String,
)

View file

@ -17,7 +17,6 @@
package io.element.android.libraries.matrix.api.verification
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@ -26,7 +25,7 @@ interface SessionVerificationService {
/**
* State of the current verification flow ([VerificationFlowState.Initial] if not started).
*/
val verificationFlowState : StateFlow<VerificationFlowState>
val verificationFlowState: StateFlow<VerificationFlowState>
/**
* The internal service that checks verification can only run after the initial sync.
@ -101,8 +100,8 @@ sealed interface VerificationFlowState {
/** Short Authentication String (SAS) verification started between the 2 devices. */
data object StartedSasVerification : VerificationFlowState
/** Verification data for the SAS verification (emojis) received. */
data class ReceivedVerificationData(val emoji: ImmutableList<VerificationEmoji>) : VerificationFlowState
/** Verification data for the SAS verification received. */
data class ReceivedVerificationData(val data: SessionVerificationData) : VerificationFlowState
/** Verification completed successfully. */
data object Finished : VerificationFlowState

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.mxc
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class MxcToolsTest {
@Test
fun `mxcUri2FilePath returns extracted path`() {
val mxcTools = MxcTools()
val mxcUri = "mxc://server.org/abc123"
val filePath = mxcTools.mxcUri2FilePath(mxcUri)
assertThat(filePath).isEqualTo("server.org/abc123")
}
@Test
fun `mxcUri2FilePath returns null for invalid data`() {
val mxcTools = MxcTools()
assertThat(mxcTools.mxcUri2FilePath("")).isNull()
assertThat(mxcTools.mxcUri2FilePath("mxc://server.org")).isNull()
assertThat(mxcTools.mxcUri2FilePath("mxc://server.org/")).isNull()
assertThat(mxcTools.mxcUri2FilePath("m://server.org/abc123")).isNull()
}
}

View file

@ -44,7 +44,7 @@ dependencies {
api(projects.libraries.matrix.api)
implementation(libs.dagger)
implementation(projects.libraries.core)
implementation("net.java.dev.jna:jna:5.13.0@aar")
implementation("net.java.dev.jna:jna:5.14.0@aar")
implementation(libs.androidx.datastore.preferences)
implementation(libs.serialization.json)
implementation(libs.kotlinx.collections.immutable)

View file

@ -91,7 +91,7 @@ import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility
import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
@OptIn(ExperimentalCoroutinesApi::class)
class RustMatrixClient constructor(
class RustMatrixClient(
private val client: Client,
private val syncService: ClientSyncService,
private val sessionStore: SessionStore,
@ -119,10 +119,9 @@ class RustMatrixClient constructor(
.filterByPushRules()
.finish()
}
private val notificationSettings = client.getNotificationSettings()
private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock)
private val notificationSettingsService = RustNotificationSettingsService(notificationSettings, dispatchers)
private val notificationSettingsService = RustNotificationSettingsService(client, dispatchers)
.apply { start() }
private val roomSyncSubscriber = RoomSyncSubscriber(innerRoomListService, dispatchers)
private val encryptionService = RustEncryptionService(
client = client,
@ -241,9 +240,8 @@ class RustMatrixClient constructor(
}
}
override suspend fun findDM(userId: UserId): MatrixRoom? {
val roomId = client.getDmRoom(userId.value)?.use { RoomId(it.id()) }
return roomId?.let { getRoom(it) }
override suspend fun findDM(userId: UserId): RoomId? {
return client.getDmRoom(userId.value)?.use { RoomId(it.id()) }
}
override suspend fun ignoreUser(userId: UserId): Result<Unit> = withContext(sessionDispatcher) {
@ -347,8 +345,7 @@ class RustMatrixClient constructor(
override fun close() {
sessionCoroutineScope.cancel()
clientDelegateTaskHandle?.cancelAndDestroy()
notificationSettings.setDelegate(null)
notificationSettings.destroy()
notificationSettingsService.destroy()
verificationService.destroy()
syncService.destroy()
innerRoomListService.destroy()

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.encryption
import io.element.android.libraries.matrix.api.encryption.RecoveryException
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.impl.exception.mapClientException
import org.matrix.rustcomponents.sdk.RecoveryException as RustRecoveryException
fun Throwable.mapRecoveryException(): RecoveryException {
return when (this) {
is RustRecoveryException.SecretStorage -> RecoveryException.SecretStorage(
message = errorMessage
)
is RustRecoveryException.BackupExistsOnServer -> RecoveryException.BackupExistsOnServer
is RustRecoveryException.Client -> RecoveryException.Client(
source.mapClientException()
)
else -> RecoveryException.Client(
ClientException.Other("Unknown error")
)
}
}

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.encryption
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
@ -110,6 +111,8 @@ internal class RustEncryptionService(
override suspend fun enableBackups(): Result<Unit> = withContext(dispatchers.io) {
runCatching {
service.enableBackups()
}.mapFailure {
it.mapRecoveryException()
}
}
@ -127,6 +130,8 @@ internal class RustEncryptionService(
)
// enableRecovery returns the encryption key, but we read it from the state flow
.let { }
}.mapFailure {
it.mapRecoveryException()
}
}
@ -164,24 +169,32 @@ internal class RustEncryptionService(
override suspend fun disableRecovery(): Result<Unit> = withContext(dispatchers.io) {
runCatching {
service.disableRecovery()
}.mapFailure {
it.mapRecoveryException()
}
}
override suspend fun isLastDevice(): Result<Boolean> = withContext(dispatchers.io) {
runCatching {
service.isLastDevice()
}.mapFailure {
it.mapRecoveryException()
}
}
override suspend fun resetRecoveryKey(): Result<String> = withContext(dispatchers.io) {
runCatching {
service.resetRecoveryKey()
}.mapFailure {
it.mapRecoveryException()
}
}
override suspend fun fixRecoveryIssues(recoveryKey: String): Result<Unit> = withContext(dispatchers.io) {
override suspend fun recover(recoveryKey: String): Result<Unit> = withContext(dispatchers.io) {
runCatching {
service.recover(recoveryKey)
}.mapFailure {
it.mapRecoveryException()
}
}
}

View file

@ -23,13 +23,13 @@ class SteadyStateExceptionMapper {
fun map(data: RustSteadyStateException): SteadyStateException {
return when (data) {
is RustSteadyStateException.BackupDisabled -> SteadyStateException.BackupDisabled(
message = data.message
message = data.message.orEmpty()
)
is RustSteadyStateException.Connection -> SteadyStateException.Connection(
message = data.message
message = data.message.orEmpty()
)
is RustSteadyStateException.Lagged -> SteadyStateException.Lagged(
message = data.message
message = data.message.orEmpty()
)
}
}

View file

@ -52,7 +52,6 @@ class NotificationMapper(
isNoisy = item.isNoisy.orFalse(),
timestamp = item.timestamp() ?: clock.epochMillis(),
content = item.event.use { notificationContentMapper.map(it) },
contentUrl = null,
hasMention = item.hasMention.orFalse(),
)
}

View file

@ -26,16 +26,16 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.NotificationSettings
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.NotificationSettingsDelegate
import org.matrix.rustcomponents.sdk.NotificationSettingsException
import timber.log.Timber
class RustNotificationSettingsService(
private val notificationSettings: NotificationSettings,
client: Client,
private val dispatchers: CoroutineDispatchers,
) : NotificationSettingsService {
private val notificationSettings = client.getNotificationSettings()
private val _notificationSettingsChangeFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val notificationSettingsChangeFlow: SharedFlow<Unit> = _notificationSettingsChangeFlow.asSharedFlow()
@ -45,10 +45,15 @@ class RustNotificationSettingsService(
}
}
init {
fun start() {
notificationSettings.setDelegate(notificationSettingsDelegate)
}
fun destroy() {
notificationSettings.setDelegate(null)
notificationSettings.destroy()
}
override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationSettings> =
runCatching {
notificationSettings.getRoomNotificationSettings(roomId.value, isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::map)
@ -119,6 +124,18 @@ class RustNotificationSettingsService(
}
}
override suspend fun isInviteForMeEnabled(): Result<Boolean> = withContext(dispatchers.io) {
runCatching {
notificationSettings.isInviteForMeEnabled()
}
}
override suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit> = withContext(dispatchers.io) {
runCatching {
notificationSettings.setInviteForMeEnabled(enabled)
}
}
override suspend fun getRoomsWithUserDefinedRules(): Result<List<String>> =
runCatching {
notificationSettings.getRoomsWithUserDefinedRules(enabled = true)

View file

@ -360,12 +360,6 @@ class RustMatrixRoom(
}
}
override suspend fun canUserJoinCall(userId: UserId): Result<Boolean> {
return runCatching {
innerRoom.canUserSendState(userId.value, StateEventType.ROOM_MEMBER_EVENT.map())
}
}
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler> {
return sendAttachment(listOf(file, thumbnailFile)) {
innerTimeline.sendImage(file.path, thumbnailFile.path, imageInfo.map(), progressCallback?.toProgressWatcher())

View file

@ -23,6 +23,7 @@ fun StateEventType.map(): RustStateEventType = when (this) {
StateEventType.POLICY_RULE_ROOM -> RustStateEventType.POLICY_RULE_ROOM
StateEventType.POLICY_RULE_SERVER -> RustStateEventType.POLICY_RULE_SERVER
StateEventType.POLICY_RULE_USER -> RustStateEventType.POLICY_RULE_USER
StateEventType.CALL_MEMBER -> RustStateEventType.CALL_MEMBER
StateEventType.ROOM_ALIASES -> RustStateEventType.ROOM_ALIASES
StateEventType.ROOM_AVATAR -> RustStateEventType.ROOM_AVATAR
StateEventType.ROOM_CANONICAL_ALIAS -> RustStateEventType.ROOM_CANONICAL_ALIAS
@ -47,6 +48,7 @@ fun RustStateEventType.map(): StateEventType = when (this) {
RustStateEventType.POLICY_RULE_ROOM -> StateEventType.POLICY_RULE_ROOM
RustStateEventType.POLICY_RULE_SERVER -> StateEventType.POLICY_RULE_SERVER
RustStateEventType.POLICY_RULE_USER -> StateEventType.POLICY_RULE_USER
RustStateEventType.CALL_MEMBER -> StateEventType.CALL_MEMBER
RustStateEventType.ROOM_ALIASES -> StateEventType.ROOM_ALIASES
RustStateEventType.ROOM_AVATAR -> StateEventType.ROOM_AVATAR
RustStateEventType.ROOM_CANONICAL_ALIAS -> StateEventType.ROOM_CANONICAL_ALIAS

View file

@ -18,12 +18,12 @@ package io.element.android.libraries.matrix.impl.verification
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -33,7 +33,8 @@ import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.SessionVerificationController
import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate
import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface
import org.matrix.rustcomponents.sdk.SessionVerificationEmoji
import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.SessionVerificationData as RustSessionVerificationData
class RustSessionVerificationService(
private val syncService: RustSyncService,
@ -105,12 +106,8 @@ class RustSessionVerificationService(
updateVerificationStatus(isVerified = true)
}
override fun didReceiveVerificationData(data: List<SessionVerificationEmoji>) {
val emojis = data.map { emoji ->
emoji.use { VerificationEmoji(it.symbol(), it.description()) }
}
.toImmutableList()
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojis)
override fun didReceiveVerificationData(data: RustSessionVerificationData) {
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(data.map())
}
// When the actual SAS verification starts
@ -142,3 +139,28 @@ class RustSessionVerificationService(
_sessionVerifiedStatus.value = newValue
}
}
private fun RustSessionVerificationData.map(): SessionVerificationData {
return use { sessionVerificationData ->
when (sessionVerificationData) {
is RustSessionVerificationData.Emojis -> {
SessionVerificationData.Emojis(
emojis = sessionVerificationData.emojis.mapIndexed { index, emoji ->
emoji.use { sessionVerificationEmoji ->
VerificationEmoji(
number = sessionVerificationData.indices[index].toInt(),
emoji = sessionVerificationEmoji.symbol(),
description = sessionVerificationEmoji.description(),
)
}
},
)
}
is RustSessionVerificationData.Decimals -> {
SessionVerificationData.Decimals(
decimals = sessionVerificationData.values.map { it.toInt() },
)
}
}
}
}

View file

@ -39,7 +39,6 @@ import io.element.android.libraries.matrix.test.media.FakeMediaLoader
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.pushers.FakePushersService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
@ -72,8 +71,7 @@ class FakeMatrixClient(
private var unignoreUserResult: Result<Unit> = Result.success(Unit)
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var createDmFailure: Throwable? = null
private var findDmResult: MatrixRoom? = FakeMatrixRoom()
private var findDmResult: RoomId? = A_ROOM_ID
private var logoutFailure: Throwable? = null
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
@ -87,7 +85,7 @@ class FakeMatrixClient(
return getRoomResults[roomId]
}
override suspend fun findDM(userId: UserId): MatrixRoom? {
override suspend fun findDM(userId: UserId): RoomId? {
return findDmResult
}
@ -99,14 +97,11 @@ class FakeMatrixClient(
return unignoreUserResult
}
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> {
delay(100)
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> = simulateLongTask {
return createRoomResult
}
override suspend fun createDM(userId: UserId): Result<RoomId> {
delay(100)
createDmFailure?.let { throw it }
override suspend fun createDM(userId: UserId): Result<RoomId> = simulateLongTask {
return createDmResult
}
@ -206,11 +201,7 @@ class FakeMatrixClient(
unignoreUserResult = result
}
fun givenCreateDmError(failure: Throwable?) {
createDmFailure = failure
}
fun givenFindDmResult(result: MatrixRoom?) {
fun givenFindDmResult(result: RoomId?) {
findDmResult = result
}

View file

@ -33,7 +33,7 @@ class FakeEncryptionService : EncryptionService {
override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Starting)
private var waitForBackupUploadSteadyStateFlow: Flow<BackupUploadState> = flowOf()
private var fixRecoveryIssuesFailure: Exception? = null
private var recoverFailure: Exception? = null
private var doesBackupExistOnServerResult: Result<Boolean> = Result.success(true)
override suspend fun enableBackups(): Result<Unit> = simulateLongTask {
@ -44,8 +44,8 @@ class FakeEncryptionService : EncryptionService {
disableRecoveryFailure = exception
}
fun givenFixRecoveryIssuesFailure(exception: Exception?) {
fixRecoveryIssuesFailure = exception
fun givenRecoverFailure(exception: Exception?) {
recoverFailure = exception
}
override suspend fun disableRecovery(): Result<Unit> = simulateLongTask {
@ -61,8 +61,8 @@ class FakeEncryptionService : EncryptionService {
return doesBackupExistOnServerResult
}
override suspend fun fixRecoveryIssues(recoveryKey: String): Result<Unit> = simulateLongTask {
fixRecoveryIssuesFailure?.let { return Result.failure(it) }
override suspend fun recover(recoveryKey: String): Result<Unit> = simulateLongTask {
recoverFailure?.let { return Result.failure(it) }
return Result.success(Unit)
}

View file

@ -41,6 +41,7 @@ class FakeNotificationSettingsService(
private var roomNotificationMode: RoomNotificationMode = initialRoomMode
private var roomNotificationModeIsDefault: Boolean = initialRoomModeIsDefault
private var callNotificationsEnabled = false
private var inviteNotificationsEnabled = false
private var atRoomNotificationsEnabled = false
private var setNotificationModeError: Throwable? = null
private var restoreDefaultNotificationModeError: Throwable? = null
@ -52,7 +53,7 @@ class FakeNotificationSettingsService(
override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationSettings> {
return Result.success(
RoomNotificationSettings(
mode = if(roomNotificationModeIsDefault) defaultEncryptedGroupRoomNotificationMode else roomNotificationMode,
mode = if (roomNotificationModeIsDefault) defaultEncryptedGroupRoomNotificationMode else roomNotificationMode,
isDefault = roomNotificationModeIsDefault
)
)
@ -149,6 +150,15 @@ class FakeNotificationSettingsService(
return Result.success(Unit)
}
override suspend fun isInviteForMeEnabled(): Result<Boolean> {
return Result.success(inviteNotificationsEnabled)
}
override suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit> {
inviteNotificationsEnabled = enabled
return Result.success(Unit)
}
override suspend fun getRoomsWithUserDefinedRules(): Result<List<String>> {
return Result.success(if (roomNotificationModeIsDefault) listOf() else listOf(A_ROOM_ID.value))
}
@ -168,5 +178,4 @@ class FakeNotificationSettingsService(
fun givenSetDefaultNotificationModeError(throwable: Throwable?) {
setDefaultNotificationModeError = throwable
}
}

View file

@ -16,12 +16,10 @@
package io.element.android.libraries.matrix.test.verification
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -31,10 +29,9 @@ class FakeSessionVerificationService : SessionVerificationService {
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
private var _canVerifySessionFlow = MutableStateFlow(true)
private var emojiList = persistentListOf<VerificationEmoji>()
var shouldFail = false
override val verificationFlowState: StateFlow<VerificationFlowState> =_verificationFlowState
override val verificationFlowState: StateFlow<VerificationFlowState> = _verificationFlowState
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
override val canVerifySessionFlow: Flow<Boolean> = _canVerifySessionFlow
@ -64,8 +61,8 @@ class FakeSessionVerificationService : SessionVerificationService {
}
}
fun triggerReceiveVerificationData() {
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList)
fun triggerReceiveVerificationData(sessionVerificationData: SessionVerificationData) {
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(sessionVerificationData)
}
override suspend fun startVerification() {
@ -88,10 +85,6 @@ class FakeSessionVerificationService : SessionVerificationService {
_isReady.value = value
}
fun givenEmojiList(emojis: List<VerificationEmoji>) {
this.emojiList = emojis.toPersistentList()
}
override suspend fun reset() {
_verificationFlowState.value = VerificationFlowState.Initial
}

View file

@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@ -80,6 +81,7 @@ private fun MatrixUserHeaderContent(
) {
// Name
Text(
modifier = Modifier.clipToBounds(),
text = matrixUser.getBestName(),
maxLines = 1,
style = ElementTheme.typography.fontHeadingSmMedium,

View file

@ -31,6 +31,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -62,6 +63,7 @@ fun SelectedUser(
) {
Avatar(matrixUser.getAvatarData(size = AvatarSize.SelectedUser))
Text(
modifier = Modifier.clipToBounds(),
text = matrixUser.getBestName(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,

View file

@ -25,12 +25,13 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.compound.theme.ElementTheme
@Composable
internal fun UserRow(
@ -55,6 +56,7 @@ internal fun UserRow(
) {
// Name
Text(
modifier = Modifier.clipToBounds(),
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,

View file

@ -28,8 +28,8 @@ import okhttp3.OkHttpClient
import javax.inject.Inject
import javax.inject.Provider
class LoggedInImageLoaderFactory @Inject constructor(
@ApplicationContext private val context: Context,
class LoggedInImageLoaderFactory(
private val context: Context,
private val matrixClient: MatrixClient,
private val okHttpClient: Provider<OkHttpClient>,
) : ImageLoaderFactory {

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.ui.media
import android.content.Context
import coil.ImageLoader
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import okhttp3.OkHttpClient
import javax.inject.Inject
import javax.inject.Provider
interface ImageLoaderHolder {
fun get(client: MatrixClient): ImageLoader
}
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultImageLoaderHolder @Inject constructor(
@ApplicationContext private val context: Context,
private val okHttpClient: Provider<OkHttpClient>,
private val sessionObserver: SessionObserver,
) : ImageLoaderHolder {
private val map = mutableMapOf<SessionId, ImageLoader>()
init {
observeSessions()
}
private fun observeSessions() {
sessionObserver.addListener(object : SessionListener {
override suspend fun onSessionCreated(userId: String) = Unit
override suspend fun onSessionDeleted(userId: String) {
map.remove(SessionId(userId))
}
})
}
override fun get(client: MatrixClient): ImageLoader {
return synchronized(map) {
map.getOrPut(client.sessionId) {
LoggedInImageLoaderFactory(
context = context,
matrixClient = client,
okHttpClient = okHttpClient,
).newImageLoader()
}
}
}
}

View file

@ -37,7 +37,7 @@ import java.util.UUID
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class PickerProviderImpl constructor(private val isInTest: Boolean) : PickerProvider {
class PickerProviderImpl(private val isInTest: Boolean) : PickerProvider {
@Inject
constructor(): this(false)

View file

@ -31,6 +31,6 @@ interface MediaPreProcessor {
compressIfPossible: Boolean
): Result<MediaUploadInfo>
data class Failure(override val cause: Throwable?) : RuntimeException(cause)
data class Failure(override val cause: Throwable?) : Exception(cause)
}

View file

@ -27,6 +27,12 @@ android {
generateDaggerFactories.set(true)
}
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
@ -37,6 +43,7 @@ android {
implementation(projects.libraries.core)
implementation(projects.libraries.di)
implementation(projects.libraries.matrix.api)
implementation(projects.services.toolbox.api)
implementation(libs.inject)
implementation(libs.androidx.exifinterface)
implementation(libs.coroutines.core)
@ -44,7 +51,10 @@ android {
implementation(libs.vanniktech.blurhash)
testImplementation(libs.test.junit)
testImplementation(libs.test.robolectric)
testImplementation(libs.coroutines.test)
testImplementation(libs.test.truth)
testImplementation(projects.tests.testutils)
testImplementation(projects.services.toolbox.test)
}
}

View file

@ -24,8 +24,8 @@ import io.element.android.libraries.androidutils.bitmap.calculateInSampleSize
import io.element.android.libraries.androidutils.bitmap.resizeToMax
import io.element.android.libraries.androidutils.bitmap.rotateToMetadataOrientation
import io.element.android.libraries.androidutils.file.createTmpFile
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStream
@ -33,8 +33,8 @@ import javax.inject.Inject
class ImageCompressor @Inject constructor(
@ApplicationContext private val context: Context,
private val dispatchers: CoroutineDispatchers,
) {
/**
* Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode], then writes it into a
* temporary file using the passed [format], [orientation] and [desiredQuality].
@ -46,7 +46,7 @@ class ImageCompressor @Inject constructor(
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
orientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
desiredQuality: Int = 80,
): Result<ImageCompressionResult> = withContext(Dispatchers.IO) {
): Result<ImageCompressionResult> = withContext(dispatchers.io) {
runCatching {
val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow()
// Encode bitmap to the destination temporary file

View file

@ -32,6 +32,7 @@ import io.element.android.libraries.androidutils.media.runAndRelease
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.media.ThumbnailInfo
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File
import javax.inject.Inject
@ -56,13 +57,14 @@ private const val VIDEO_THUMB_FRAME = 0L
class ThumbnailFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val sdkIntProvider: BuildVersionSdkIntProvider
) {
@SuppressLint("NewApi")
suspend fun createImageThumbnail(file: File): ThumbnailResult {
return createThumbnail { cancellationSignal ->
// This API works correctly with GIF
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (sdkIntProvider.isAtLeast(Build.VERSION_CODES.Q)) {
ThumbnailUtils.createImageThumbnail(
file,
Size(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT),

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6831610b21668c49e31732f9005177e959277233d3cab758910e061294f91d79
size 687979

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a980f7b74cb9edc323919db8652798da4b3dcf865fc7b6a1eb1110096b7bfb4f
size 1856786

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0244590f2b4bcb62352b574e78bea940e8d89cfa69823b5208ef4c43e0abcb44
size 52079

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ba904eae8773b70c75333db4de2f3ac45a8ad4ddba1b242f0b3cfc199391dd8
size 13

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fb58436524db95bd0c10b2c3023c2eb7b87404a2eab8987939f051647eb859d3
size 1673712

View file

@ -0,0 +1,347 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.mediaupload
import android.content.Context
import android.os.Build
import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.ThumbnailInfo
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import kotlin.time.Duration
@RunWith(RobolectricTestRunner::class)
class AndroidMediaPreProcessorTest {
@Test
fun `test processing image`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
val sut = createAndroidMediaPreProcessor(context)
val file = getFileFromAssets(context, "image.png")
val result = sut.process(
uri = file.toUri(),
mimeType = MimeTypes.Png,
deleteOriginal = false,
compressIfPossible = true,
)
// This is failing for now
val error = result.exceptionOrNull()
assertThat(error).isInstanceOf(MediaPreProcessor.Failure::class.java)
assertThat(error?.cause).isInstanceOf(NullPointerException::class.java)
/*
val data = result.getOrThrow()
assertThat(data.file.path).endsWith("image.png")
val info = data as MediaUploadInfo.Image
assertThat(info.thumbnailFile).isNull() // TODO Check this
assertThat(info.imageInfo).isEqualTo(
ImageInfo(
height = 1_178,
width = 1_818,
mimetype = MimeTypes.Png,
size = 114_867,
thumbnailInfo = null,
thumbnailSource = null,
blurhash = null,
)
)
assertThat(file.exists()).isTrue()
*/
}
@Test
fun `test processing image api Q`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
val sut = createAndroidMediaPreProcessor(context, sdkIntVersion = Build.VERSION_CODES.Q)
val file = getFileFromAssets(context, "image.png")
val result = sut.process(
uri = file.toUri(),
mimeType = MimeTypes.Png,
deleteOriginal = false,
compressIfPossible = true,
)
// This is not working for now
val error = result.exceptionOrNull()
assertThat(error).isInstanceOf(MediaPreProcessor.Failure::class.java)
assertThat(error?.cause).isInstanceOf(NoSuchMethodError::class.java)
/*
val data = result.getOrThrow()
assertThat(data.file.path).endsWith("image.png")
val info = data as MediaUploadInfo.Image
assertThat(info.thumbnailFile).isNull() // TODO Check this
assertThat(info.imageInfo).isEqualTo(
ImageInfo(
height = 1_178,
width = 1_818,
mimetype = MimeTypes.Png,
size = 114_867,
thumbnailInfo = null,
thumbnailSource = null,
blurhash = null,
)
)
assertThat(file.exists()).isTrue()
*/
}
@Test
fun `test processing image no compression`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
val sut = createAndroidMediaPreProcessor(context)
val file = getFileFromAssets(context, "image.png")
val result = sut.process(
uri = file.toUri(),
mimeType = MimeTypes.Png,
deleteOriginal = false,
compressIfPossible = false,
).getOrThrow()
assertThat(result.file.path).endsWith("image.png")
val info = result as MediaUploadInfo.Image
assertThat(info.thumbnailFile).isNotNull()
assertThat(info.imageInfo).isEqualTo(
ImageInfo(
height = 1_178,
width = 1_818,
mimetype = MimeTypes.Png,
size = 1_856_786,
thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Jpeg, size = 643),
thumbnailSource = null,
blurhash = "K00000fQfQfQfQfQfQfQfQ",
)
)
assertThat(file.exists()).isTrue()
}
@Test
fun `test processing image and delete`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
val sut = createAndroidMediaPreProcessor(context)
val file = getFileFromAssets(context, "image.png")
val result = sut.process(
uri = file.toUri(),
mimeType = MimeTypes.Png,
deleteOriginal = true,
compressIfPossible = false,
).getOrThrow()
assertThat(result.file.path).endsWith("image.png")
val info = result as MediaUploadInfo.Image
assertThat(info.thumbnailFile).isNotNull()
assertThat(info.imageInfo).isEqualTo(
ImageInfo(
height = 1_178,
width = 1_818,
mimetype = MimeTypes.Png,
size = 1_856_786,
thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Jpeg, size = 643),
thumbnailSource = null,
blurhash = "K00000fQfQfQfQfQfQfQfQ",
)
)
// Does not work
// assertThat(file.exists()).isFalse()
}
@Test
fun `test processing gif`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
val sut = createAndroidMediaPreProcessor(context)
val file = getFileFromAssets(context, "animated_gif.gif")
val result = sut.process(
uri = file.toUri(),
mimeType = MimeTypes.Gif,
deleteOriginal = false,
compressIfPossible = true,
).getOrThrow()
assertThat(result.file.path).endsWith("animated_gif.gif")
val info = result as MediaUploadInfo.Image
assertThat(info.thumbnailFile).isNotNull()
assertThat(info.imageInfo).isEqualTo(
ImageInfo(
height = 600,
width = 800,
mimetype = MimeTypes.Gif,
size = 687_979,
thumbnailInfo = ThumbnailInfo(height = 50, width = 50, mimetype = MimeTypes.Jpeg, size = 691),
thumbnailSource = null,
blurhash = "K00000fQfQfQfQfQfQfQfQ",
)
)
assertThat(file.exists()).isTrue()
}
@Test
fun `test processing file`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
val sut = createAndroidMediaPreProcessor(context)
val file = getFileFromAssets(context, "text.txt")
val result = sut.process(
uri = file.toUri(),
mimeType = MimeTypes.PlainText,
deleteOriginal = false,
compressIfPossible = true,
).getOrThrow()
assertThat(result.file.path).endsWith("text.txt")
val info = result as MediaUploadInfo.AnyFile
assertThat(info.fileInfo).isEqualTo(
FileInfo(
mimetype = MimeTypes.PlainText,
size = 13,
thumbnailInfo = null,
thumbnailSource = null,
)
)
assertThat(file.exists()).isTrue()
}
@Ignore("Compressing video is not working with Robolectric")
@Test
fun `test processing video`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
val sut = createAndroidMediaPreProcessor(context)
val file = getFileFromAssets(context, "video.mp4")
val result = sut.process(
uri = file.toUri(),
mimeType = MimeTypes.Mp4,
deleteOriginal = false,
compressIfPossible = true,
).getOrThrow()
assertThat(result.file.path).endsWith("video.mp4")
val info = result as MediaUploadInfo.Video
assertThat(info.thumbnailFile).isNotNull()
assertThat(info.videoInfo).isEqualTo(
VideoInfo(
duration = Duration.ZERO, // Not available with Robolectric?
height = 1_178,
width = 1_818,
mimetype = MimeTypes.Mp4,
size = 114_867,
thumbnailInfo = null,
thumbnailSource = null,
blurhash = null,
)
)
assertThat(file.exists()).isTrue()
}
@Test
fun `test processing video no compression`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
val sut = createAndroidMediaPreProcessor(context)
val file = getFileFromAssets(context, "video.mp4")
val result = sut.process(
uri = file.toUri(),
mimeType = MimeTypes.Mp4,
deleteOriginal = false,
compressIfPossible = false,
).getOrThrow()
assertThat(result.file.path).endsWith("video.mp4")
val info = result as MediaUploadInfo.Video
assertThat(info.thumbnailFile).isNotNull()
assertThat(info.videoInfo).isEqualTo(
VideoInfo(
duration = Duration.ZERO, // Not available with Robolectric?
height = 0, // Not available with Robolectric?
width = 0, // Not available with Robolectric?
mimetype = MimeTypes.Mp4,
size = 1_673_712,
thumbnailInfo = ThumbnailInfo(height = null, width = null, mimetype = MimeTypes.Jpeg, size = 0), // Not available with Robolectric?
thumbnailSource = null,
blurhash = null,
)
)
assertThat(file.exists()).isTrue()
}
@Test
fun `test processing audio`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
val sut = createAndroidMediaPreProcessor(context)
val file = getFileFromAssets(context, "sample3s.mp3")
val result = sut.process(
uri = file.toUri(),
mimeType = MimeTypes.Mp3,
deleteOriginal = false,
compressIfPossible = true,
).getOrThrow()
assertThat(result.file.path).endsWith("sample3s.mp3")
val info = result as MediaUploadInfo.Audio
assertThat(info.audioInfo).isEqualTo(
AudioInfo(
duration = Duration.ZERO, // Not available with Robolectric?
size = 52_079,
mimetype = MimeTypes.Mp3,
)
)
assertThat(file.exists()).isTrue()
}
@Test
fun `test file which does not exist`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
val sut = createAndroidMediaPreProcessor(context)
val file = File(context.cacheDir, "not found.txt")
val result = sut.process(
uri = file.toUri(),
mimeType = MimeTypes.PlainText,
deleteOriginal = false,
compressIfPossible = true,
)
assertThat(result.isFailure).isTrue()
val failure = result.exceptionOrNull()
assertThat(failure).isInstanceOf(MediaPreProcessor.Failure::class.java)
assertThat(failure?.cause).isInstanceOf(FileNotFoundException::class.java)
}
private fun TestScope.createAndroidMediaPreProcessor(
context: Context,
sdkIntVersion: Int = Build.VERSION_CODES.P
) = AndroidMediaPreProcessor(
context = context,
thumbnailFactory = ThumbnailFactory(context, FakeBuildVersionSdkIntProvider(sdkIntVersion)),
imageCompressor = ImageCompressor(context, testCoroutineDispatchers()),
videoCompressor = VideoCompressor(context),
coroutineDispatchers = testCoroutineDispatchers(),
)
@Throws(IOException::class)
private fun getFileFromAssets(context: Context, fileName: String): File = File(context.cacheDir, fileName)
.also {
if (!it.exists()) {
it.outputStream().use { cache ->
context.assets.open(fileName).use { inputStream ->
inputStream.copyTo(cache)
}
}
}
}
}

View file

@ -55,6 +55,7 @@ dependencies {
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.mockk)
testImplementation(libs.test.robolectric)
testImplementation(libs.test.turbine)
testImplementation(libs.coroutines.core)
testImplementation(libs.coroutines.test)

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.mediaviewer.api.util
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class FileExtensionExtractorTest {
@Test
fun `test FileExtensionExtractor with validation OK`() {
val sut = FileExtensionExtractorWithValidation()
// The result should be txt, but with Robolectric,
// MimeTypeMap.getSingleton().hasExtension() always returns false
assertThat(sut.extractFromName("test.txt")).isEqualTo("bin")
}
@Test
fun `test FileExtensionExtractor with validation ERROR`() {
val sut = FileExtensionExtractorWithValidation()
assertThat(sut.extractFromName("test.bla")).isEqualTo("bin")
}
@Test
fun `test FileExtensionExtractor no validation`() {
val sut = FileExtensionExtractorWithoutValidation()
assertThat(sut.extractFromName("test.png")).isEqualTo("png")
assertThat(sut.extractFromName("test.bla")).isEqualTo("bla")
}
}

View file

@ -46,6 +46,9 @@ dependencies {
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.mockk)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.turbine)
testImplementation(libs.test.robolectric)
testImplementation(libs.test.turbine)
testImplementation(libs.coroutines.core)
testImplementation(libs.coroutines.test)

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.libraries.mediaviewer.local
package io.element.android.libraries.mediaviewer.impl.local
import android.app.Activity
import android.content.ContentResolver

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.libraries.mediaviewer.local
package io.element.android.libraries.mediaviewer.impl.local
import android.content.Context
import android.net.Uri

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.mediaviewer.impl.local
import android.net.Uri
import androidx.activity.compose.LocalActivityResultRegistryOwner
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class AndroidLocalMediaActionsTest {
@Test
fun `present - AndroidLocalMediaAction configure`() = runTest {
val sut = createAndroidLocalMediaActions()
moleculeFlow(RecompositionMode.Immediate) {
CompositionLocalProvider(
LocalContext provides RuntimeEnvironment.getApplication(),
LocalActivityResultRegistryOwner provides NoOpActivityResultRegistryOwner()
) {
sut.Configure()
}
}.test {
awaitItem()
}
}
@Test
fun `test AndroidLocalMediaAction share`() = runTest {
val sut = createAndroidLocalMediaActions()
val result = sut.share(aLocalMedia(Uri.parse("file://afile")))
assertThat(result.exceptionOrNull()).isNotNull()
}
@Test
fun `test AndroidLocalMediaAction open`() = runTest {
val sut = createAndroidLocalMediaActions()
val result = sut.open(aLocalMedia(Uri.parse("file://afile")))
assertThat(result.exceptionOrNull()).isNotNull()
}
@Test
fun `test AndroidLocalMediaAction save on disk`() = runTest {
val sut = createAndroidLocalMediaActions()
val result = sut.saveOnDisk(aLocalMedia(Uri.parse("file://afile")))
assertThat(result.exceptionOrNull()).isNotNull()
}
private fun TestScope.createAndroidLocalMediaActions() = AndroidLocalMediaActions(
RuntimeEnvironment.getApplication(),
testCoroutineDispatchers(),
aBuildMeta()
)
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.mediaviewer.impl.local
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.test.media.FakeMediaFile
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.anImageInfo
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class AndroidLocalMediaFactoryTest {
@Test
fun `test AndroidLocalMediaFactory`() {
val sut = createAndroidLocalMediaFactory()
val result = sut.createFromMediaFile(aMediaFile(), anImageInfo())
assertThat(result.uri.toString()).endsWith("aPath")
assertThat(result.info).isEqualTo(
MediaInfo(
name = "an image file.jpg",
mimeType = MimeTypes.Jpeg,
formattedFileSize = "4MB",
fileExtension = "jpg",
)
)
}
private fun aMediaFile(): MediaFile {
return FakeMediaFile("aPath")
}
private fun createAndroidLocalMediaFactory(): AndroidLocalMediaFactory {
return AndroidLocalMediaFactory(
RuntimeEnvironment.getApplication(),
FakeFileSizeFormatter(),
FileExtensionExtractorWithoutValidation()
)
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.mediaviewer.impl.local
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.ActivityResultRegistryOwner
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.app.ActivityOptionsCompat
class NoOpActivityResultRegistryOwner : ActivityResultRegistryOwner {
override val activityResultRegistry: ActivityResultRegistry
get() = NoOpActivityResultRegistry()
}
class NoOpActivityResultRegistry : ActivityResultRegistry() {
override fun <I : Any?, O : Any?> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?,
) = Unit
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Hogy az alkalmazás használhassa a kamerát, adja meg az engedélyt a rendszerbeállításokban."</string>
<string name="dialog_permission_generic">"Adja meg az engedélyt a rendszerbeállításokban."</string>
<string name="dialog_permission_microphone">"Hogy az alkalmazás használhassa a mikrofont, adja meg az engedélyt a rendszerbeállításokban."</string>
<string name="dialog_permission_notification">"Hogy az alkalmazás megjeleníthesse az értesítéseket, adja meg az engedélyt a rendszerbeállításokban."</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Supaya aplikasinya dapat menggunakan kamera, berikan izin dalam pengaturan sistem."</string>
<string name="dialog_permission_generic">"Silakan memberikan izin dalam pengaturan sistem."</string>
<string name="dialog_permission_microphone">"Supaya aplikasinya dapat menggunakan mikrofon, berikan izin dalam pengaturan sistem."</string>
<string name="dialog_permission_notification">"Supaya aplikasinya dapat menampilkan notifikasi, berikan izin dalam pengaturan sistem."</string>
</resources>

View file

@ -27,7 +27,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.PermissionStatus
class FakeComposablePermissionStateProvider constructor(
class FakeComposablePermissionStateProvider(
private val permissionState: FakePermissionState
) : ComposablePermissionStateProvider {
private lateinit var onPermissionResult: (Boolean) -> Unit

View file

@ -15,6 +15,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<receiver
@ -24,5 +25,17 @@
android:name=".notifications.NotificationBroadcastReceiver"
android:enabled="true"
android:exported="false" />
<provider
android:name=".notifications.NotificationsFileProvider"
android:authorities="${applicationId}.notifications.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/notifications_provider_paths" />
</provider>
</application>
</manifest>

View file

@ -30,6 +30,7 @@ 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.ThreadId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.services.appnavstate.api.AppNavigationStateService
@ -61,6 +62,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val buildMeta: BuildMeta,
private val matrixClientProvider: MatrixClientProvider,
private val imageLoaderHolder: ImageLoaderHolder,
) : NotificationDrawerManager {
private var appNavigationStateObserver: Job? = null
@ -288,10 +290,11 @@ class DefaultNotificationDrawerManager @Inject constructor(
}
eventsForSessions.forEach { (sessionId, notifiableEvents) ->
val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow()
val imageLoader = imageLoaderHolder.get(client)
val currentUser = tryOrNull(
onError = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") },
operation = {
val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow()
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
val myUserDisplayName = client.loadUserDisplayName().getOrNull() ?: sessionId.value
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
@ -307,7 +310,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
avatarUrl = null
)
notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents)
notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents, imageLoader)
}
}
}

View file

@ -16,7 +16,12 @@
package io.element.android.libraries.push.impl.notifications
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
@ -60,6 +65,8 @@ class NotifiableEventResolver @Inject constructor(
private val stringProvider: StringProvider,
private val clock: SystemClock,
private val matrixClientProvider: MatrixClientProvider,
private val notificationMediaRepoFactory: NotificationMediaRepo.Factory,
@ApplicationContext private val context: Context,
) {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
@ -75,10 +82,13 @@ class NotifiableEventResolver @Inject constructor(
}.getOrNull()
// TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event
return notificationData?.asNotifiableEvent(sessionId)
return notificationData?.asNotifiableEvent(client, sessionId)
}
private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent? {
private suspend fun NotificationData.asNotifiableEvent(
client: MatrixClient,
userId: SessionId,
): NotifiableEvent? {
return when (val content = this.content) {
is NotificationContent.MessageLike.RoomMessage -> {
val messageBody = descriptionFromMessageContent(content, senderDisplayName ?: content.senderId.value)
@ -96,7 +106,7 @@ class NotifiableEventResolver @Inject constructor(
timestamp = this.timestamp,
senderName = senderDisplayName,
body = notificationBody,
imageUriString = this.contentUrl,
imageUriString = fetchImageIfPresent(client)?.toString(),
roomName = roomDisplayName,
roomIsDirect = isDirect,
roomAvatarPath = roomAvatarUrl,
@ -238,6 +248,34 @@ class NotifiableEventResolver @Inject constructor(
stringProvider.getString(R.string.notification_room_invite_body)
}
}
private suspend fun NotificationData.fetchImageIfPresent(client: MatrixClient): Uri? {
val fileResult = when (val content = this.content) {
is NotificationContent.MessageLike.RoomMessage -> {
when (val messageType = content.messageType) {
is ImageMessageType -> notificationMediaRepoFactory.create(client)
.getMediaFile(
mediaSource = messageType.source,
mimeType = messageType.info?.mimetype,
body = messageType.body,
)
is VideoMessageType -> null // Use the thumbnail here?
else -> null
}
}
else -> null
} ?: return null
return fileResult
.onFailure {
Timber.tag(loggerTag.value).e(it, "Failed to download image for notification")
}
.map { mediaFile ->
val authority = "${context.packageName}.notifications.fileprovider"
FileProvider.getUriForFile(context, authority, mediaFile)
}
.getOrNull()
}
}
@Suppress("LongParameterList")

View file

@ -21,7 +21,7 @@ import android.graphics.Bitmap
import android.os.Build
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap
import coil.imageLoader
import coil.ImageLoader
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import io.element.android.libraries.di.ApplicationContext
@ -39,21 +39,22 @@ class NotificationBitmapLoader @Inject constructor(
/**
* Get icon of a room.
* @param path mxc url
* @param imageLoader Coil image loader
*/
suspend fun getRoomBitmap(path: String?): Bitmap? {
suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? {
if (path == null) {
return null
}
return loadRoomBitmap(path)
return loadRoomBitmap(path, imageLoader)
}
private suspend fun loadRoomBitmap(path: String): Bitmap? {
private suspend fun loadRoomBitmap(path: String, imageLoader: ImageLoader): Bitmap? {
return try {
val imageRequest = ImageRequest.Builder(context)
.data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
.transformations(CircleCropTransformation())
.build()
val result = context.imageLoader.execute(imageRequest)
val result = imageLoader.execute(imageRequest)
result.drawable?.toBitmap()
} catch (e: Throwable) {
Timber.e(e, "Unable to load room bitmap")
@ -65,22 +66,23 @@ class NotificationBitmapLoader @Inject constructor(
* Get icon of a user.
* Before Android P, this does nothing because the icon won't be used
* @param path mxc url
* @param imageLoader Coil image loader
*/
suspend fun getUserIcon(path: String?): IconCompat? {
suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? {
if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) {
return null
}
return loadUserIcon(path)
return loadUserIcon(path, imageLoader)
}
private suspend fun loadUserIcon(path: String): IconCompat? {
private suspend fun loadUserIcon(path: String, imageLoader: ImageLoader): IconCompat? {
return try {
val imageRequest = ImageRequest.Builder(context)
.data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
.transformations(CircleCropTransformation())
.build()
val result = context.imageLoader.execute(imageRequest)
val result = imageLoader.execute(imageRequest)
val bitmap = result.drawable?.toBitmap()
return bitmap?.let { IconCompat.createWithBitmap(it) }
} catch (e: Throwable) {

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.push.impl.notifications
import android.app.Notification
import coil.ImageLoader
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
@ -36,6 +37,7 @@ class NotificationFactory @Inject constructor(
suspend fun Map<RoomId, ProcessedMessageEvents>.toNotifications(
currentUser: MatrixUser,
imageLoader: ImageLoader,
): List<RoomNotification> {
return map { (roomId, events) ->
when {
@ -46,6 +48,7 @@ class NotificationFactory @Inject constructor(
currentUser = currentUser,
events = messageEvents,
roomId = roomId,
imageLoader = imageLoader,
)
}
}

View file

@ -0,0 +1,120 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications
import com.squareup.anvil.annotations.ContributesBinding
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.mxc.MxcTools
import java.io.File
/**
* Fetches the media file for a notification.
*
* Media is downloaded from the rust sdk and stored in the application's cache directory.
* Media files are indexed by their Matrix Content (mxc://) URI and considered immutable.
* Whenever a given mxc is found in the cache, it is returned immediately.
*/
interface NotificationMediaRepo {
/**
* Factory for [NotificationMediaRepo].
*/
fun interface Factory {
/**
* Creates a [NotificationMediaRepo].
*
*/
fun create(
client: MatrixClient
): NotificationMediaRepo
}
/**
* Returns the file.
*
* In case of a cache hit the file is returned immediately.
* In case of a cache miss the file is downloaded and then returned.
*
* @param mediaSource the media source of the media.
* @param mimeType the mime type of the media.
* @param body the body of the message.
* @return A [Result] holding either the media [File] from the cache directory or an [Exception].
*/
suspend fun getMediaFile(
mediaSource: MediaSource,
mimeType: String?,
body: String?,
): Result<File>
}
class DefaultNotificationMediaRepo @AssistedInject constructor(
@CacheDirectory private val cacheDir: File,
private val mxcTools: MxcTools,
@Assisted private val client: MatrixClient,
) : NotificationMediaRepo {
@ContributesBinding(AppScope::class)
@AssistedFactory
fun interface Factory : NotificationMediaRepo.Factory {
override fun create(
client: MatrixClient,
): DefaultNotificationMediaRepo
}
private val matrixMediaLoader = client.mediaLoader
override suspend fun getMediaFile(
mediaSource: MediaSource,
mimeType: String?,
body: String?,
): Result<File> {
val cachedFile = mediaSource.cachedFile()
return when {
cachedFile == null -> Result.failure(IllegalStateException("Invalid mxcUri."))
cachedFile.exists() -> Result.success(cachedFile)
else -> matrixMediaLoader.downloadMediaFile(
source = mediaSource,
mimeType = mimeType,
body = body,
).mapCatching {
it.use { mediaFile ->
val dest = cachedFile.apply { parentFile?.mkdirs() }
if (mediaFile.persist(dest.path)) {
dest
} else {
error("Failed to move file to cache.")
}
}
}
}
}
private fun MediaSource.cachedFile(): File? = mxcTools.mxcUri2FilePath(url)?.let {
File("${cacheDir.path}/$CACHE_NOTIFICATION_SUBDIR/$it")
}
}
/**
* Subdirectory of the application's cache directory where file are stored.
*/
private const val CACHE_NOTIFICATION_SUBDIR = "temp/notif"

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.push.impl.notifications
import coil.ImageLoader
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
@ -38,11 +39,12 @@ class NotificationRenderer @Inject constructor(
suspend fun render(
currentUser: MatrixUser,
useCompleteNotificationFormat: Boolean,
eventsToProcess: List<ProcessedEvent<NotifiableEvent>>
eventsToProcess: List<ProcessedEvent<NotifiableEvent>>,
imageLoader: ImageLoader,
) {
val groupedEvents = eventsToProcess.groupByType()
with(notificationFactory) {
val roomNotifications = groupedEvents.roomEvents.toNotifications(currentUser)
val roomNotifications = groupedEvents.roomEvents.toNotifications(currentUser, imageLoader)
val invitationNotifications = groupedEvents.invitationEvents.toNotifications()
val simpleNotifications = groupedEvents.simpleEvents.toNotifications()
val fallbackNotifications = groupedEvents.fallbackEvents.toNotifications()

View file

@ -14,9 +14,12 @@
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.verification
package io.element.android.libraries.push.impl.notifications
data class VerificationEmoji(
val code: String,
val name: String,
)
import androidx.core.content.FileProvider
/**
* We have to declare our own file provider to avoid collision with other modules
* having their own.
*/
class NotificationsFileProvider : FileProvider()

View file

@ -23,6 +23,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import coil.ImageLoader
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.R
@ -42,6 +43,7 @@ class RoomGroupMessageCreator @Inject constructor(
currentUser: MatrixUser,
events: List<NotifiableMessageEvent>,
roomId: RoomId,
imageLoader: ImageLoader,
): RoomNotification.Message {
val lastKnownRoomEvent = events.last()
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "Room name (${roomId.value.take(8)}…)"
@ -49,13 +51,13 @@ class RoomGroupMessageCreator @Inject constructor(
val style = NotificationCompat.MessagingStyle(
Person.Builder()
.setName(currentUser.displayName?.annotateForDebug(50))
.setIcon(bitmapLoader.getUserIcon(currentUser.avatarUrl))
.setIcon(bitmapLoader.getUserIcon(currentUser.avatarUrl, imageLoader))
.setKey(lastKnownRoomEvent.sessionId.value)
.build()
).also {
it.conversationTitle = roomName.takeIf { roomIsGroup }?.annotateForDebug(51)
it.isGroupConversation = roomIsGroup
it.addMessagesFromEvents(events)
it.addMessagesFromEvents(events, imageLoader)
}
val tickerText = if (roomIsGroup) {
@ -64,7 +66,7 @@ class RoomGroupMessageCreator @Inject constructor(
stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description)
}
val largeBitmap = getRoomBitmap(events)
val largeBitmap = getRoomBitmap(events, imageLoader)
val lastMessageTimestamp = events.last().timestamp
val smartReplyErrors = events.filter { it.isSmartReplyError() }
@ -98,14 +100,17 @@ class RoomGroupMessageCreator @Inject constructor(
)
}
private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List<NotifiableMessageEvent>) {
private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents(
events: List<NotifiableMessageEvent>,
imageLoader: ImageLoader,
) {
events.forEach { event ->
val senderPerson = if (event.outGoingMessage) {
null
} else {
Person.Builder()
.setName(event.senderName?.annotateForDebug(70))
.setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath))
.setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath, imageLoader))
.setKey(event.senderId.value)
.build()
}
@ -167,10 +172,13 @@ class RoomGroupMessageCreator @Inject constructor(
}
}
private suspend fun getRoomBitmap(events: List<NotifiableMessageEvent>): Bitmap? {
private suspend fun getRoomBitmap(
events: List<NotifiableMessageEvent>,
imageLoader: ImageLoader,
): Bitmap? {
// Use the last event (most recent?)
return events.reversed().firstNotNullOfOrNull { it.roomAvatarPath }
?.let { bitmapLoader.getRoomBitmap(it) }
?.let { bitmapLoader.getRoomBitmap(it, imageLoader) }
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View file

@ -58,9 +58,8 @@ data class NotifiableMessageEvent(
override val description: String = body ?: ""
val title: String = senderName ?: ""
// TODO EAx The image has to be downloaded and expose using the file provider.
// Example of value from Element Android:
// content://im.vector.app.debug.mx-sdk.fileprovider/downloads/downloads/816abf76d806c768760568952b1862c8/F/72c33edd23dee3b95f4d5a18aa25fa54/image.png
// Example of value:
// content://io.element.android.x.debug.notifications.fileprovider/downloads/temp/notif/matrix.org/XGItzSDOnSyXjYtOPfiKexDJ
val imageUri: Uri?
get() = imageUriString?.let { Uri.parse(it) }
}

View file

@ -8,6 +8,7 @@
<string name="notification_invitation_action_join">"Rejoindre"</string>
<string name="notification_invitation_action_reject">"Rejeter"</string>
<string name="notification_invite_body">"Vous a invité(e) à discuter"</string>
<string name="notification_mentioned_you_body">"Mentionné(e): %1$s"</string>
<string name="notification_new_messages">"Nouveaux messages"</string>
<string name="notification_reaction_body">"A réagi avec %1$s"</string>
<string name="notification_room_action_mark_as_read">"Marquer comme lu"</string>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_call">"Hívás"</string>
<string name="notification_channel_listening_for_events">"Események figyelése"</string>
<string name="notification_channel_noisy">"Zajos értesítések"</string>
<string name="notification_channel_silent">"Csendes értesítések"</string>
<string name="notification_inline_reply_failed">"** Nem sikerült elküldeni nyissa meg a szobát"</string>
<string name="notification_invitation_action_join">"Csatlakozás"</string>
<string name="notification_invitation_action_reject">"Elutasítás"</string>
<string name="notification_invite_body">"Meghívta, hogy csevegjen"</string>
<string name="notification_mentioned_you_body">"Megemlítette Önt: %1$s"</string>
<string name="notification_new_messages">"Új üzenetek"</string>
<string name="notification_reaction_body">"Ezzel reagált: %1$s"</string>
<string name="notification_room_action_mark_as_read">"Megjelölés olvasottként"</string>
<string name="notification_room_invite_body">"Meghívta, hogy csatlakozzon a szobához"</string>
<string name="notification_sender_me">"Én"</string>
<string name="notification_test_push_notification_content">"Az értesítést nézi! Kattintson ide!"</string>
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
<string name="notification_unread_notified_messages_and_invitation">"%1$s és %2$s"</string>
<string name="notification_unread_notified_messages_in_room">"%1$s itt: %2$s"</string>
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s itt: %2$s és %3$s"</string>
<string name="push_choose_distributor_dialog_title_android">"Válassza ki az értesítések fogadásának módját"</string>
<string name="push_distributor_background_sync_android">"Háttérszinkronizálás"</string>
<string name="push_distributor_firebase_android">"Google szolgáltatások"</string>
<string name="push_no_valid_google_play_services_apk_android">"A Google Play szolgáltatások nem találhatók. Előfordulhat, hogy az értesítések nem működnek megfelelően."</string>
<string name="notification_fallback_content">"Értesítés"</string>
<string name="notification_room_action_quick_reply">"Gyors válasz"</string>
</resources>

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_call">"Panggil"</string>
<string name="notification_channel_listening_for_events">"Mendengarkan peristiwa"</string>
<string name="notification_channel_noisy">"Pemberitahuan berisik"</string>
<string name="notification_channel_silent">"Pemberitahuan diam"</string>
<string name="notification_inline_reply_failed">"** Gagal mengirim — silakan buka ruangan"</string>
<string name="notification_invitation_action_join">"Bergabung"</string>
<string name="notification_invitation_action_reject">"Tolak"</string>
<string name="notification_invite_body">"Mengundang Anda untuk mengobrol"</string>
<string name="notification_mentioned_you_body">"Menyebutkan Anda: %1$s"</string>
<string name="notification_new_messages">"Pesan Baru"</string>
<string name="notification_reaction_body">"Menghapus dengan %1$s"</string>
<string name="notification_room_action_mark_as_read">"Tandai sebagai dibaca"</string>
<string name="notification_room_invite_body">"Mengundang Anda untuk bergabung ke ruangan"</string>
<string name="notification_sender_me">"Saya"</string>
<string name="notification_test_push_notification_content">"Anda sedang melihat pemberitahuan ini! Klik saya!"</string>
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
<string name="notification_unread_notified_messages_and_invitation">"%1$s dan %2$s"</string>
<string name="notification_unread_notified_messages_in_room">"%1$s di %2$s"</string>
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s di %2$s dan %3$s"</string>
<plurals name="notification_compat_summary_line_for_room">
<item quantity="other">"%1$s: %2$d pesan"</item>
</plurals>
<plurals name="notification_compat_summary_title">
<item quantity="other">"%d pemberitahuan"</item>
</plurals>
<plurals name="notification_invitations">
<item quantity="other">"%d undangan"</item>
</plurals>
<plurals name="notification_new_messages_for_room">
<item quantity="other">"%d pesan baru"</item>
</plurals>
<plurals name="notification_unread_notified_messages">
<item quantity="other">"%d pesan pemberitahuan yang belum dibaca"</item>
</plurals>
<plurals name="notification_unread_notified_messages_in_room_rooms">
<item quantity="other">"%d ruangan"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Pilih cara menerima notifikasi"</string>
<string name="push_distributor_background_sync_android">"Sinkronisasi latar belakang"</string>
<string name="push_distributor_firebase_android">"Layanan Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Tidak ditemukan Layanan Google Play yang valid. Pemberitahuan mungkin tidak berfungsi dengan baik."</string>
<string name="notification_fallback_content">"Notifikasi"</string>
<string name="notification_room_action_quick_reply">"Balas cepat"</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path
name="downloads"
path="/" />
</paths>

View file

@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoaderHolder
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
@ -129,6 +130,7 @@ class DefaultNotificationDrawerManagerTest {
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
buildMeta = aBuildMeta(),
matrixClientProvider = FakeMatrixClientProvider(),
imageLoaderHolder = FakeImageLoaderHolder(),
)
}
}

View file

@ -42,6 +42,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationMediaRepo
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@ -257,7 +258,7 @@ class NotifiableEventResolverTest {
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2,
messageType = LocationMessageType("Location", "geo:1,2", null),
messageType = LocationMessageType("Location", "geo:1,2", null),
)
)
)
@ -274,7 +275,7 @@ class NotifiableEventResolverTest {
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2,
messageType = NoticeMessageType("Notice", null),
messageType = NoticeMessageType("Notice", null),
)
)
)
@ -291,7 +292,7 @@ class NotifiableEventResolverTest {
createNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2,
messageType = EmoteMessageType("is happy", null),
messageType = EmoteMessageType("is happy", null),
)
)
)
@ -487,11 +488,15 @@ class NotifiableEventResolverTest {
Result.success(FakeMatrixClient(notificationService = notificationService))
}
})
val notificationMediaRepoFactory = NotificationMediaRepo.Factory {
FakeNotificationMediaRepo()
}
return NotifiableEventResolver(
stringProvider = AndroidStringProvider(context.resources),
clock = FakeSystemClock(),
matrixClientProvider = matrixClientProvider,
notificationMediaRepoFactory = notificationMediaRepoFactory,
context = context,
)
}
@ -512,7 +517,6 @@ class NotifiableEventResolverTest {
isNoisy = false,
timestamp = A_TIMESTAMP,
content = content,
contentUrl = null,
hasMention = hasMention,
)
}

View file

@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
@ -30,12 +31,15 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNoti
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
private val MY_AVATAR_URL: String? = null
private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID)
private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID)
private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
@RunWith(RobolectricTestRunner::class)
class NotificationFactoryTest {
private val androidNotificationFactory = FakeAndroidNotificationFactory()
@ -130,11 +134,14 @@ class NotificationFactoryTest {
)
val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT)))
val fakeImageLoader = FakeImageLoader()
val result = roomWithMessage.toNotifications(
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
imageLoader = fakeImageLoader.getImageLoader(),
)
assertThat(result).isEqualTo(listOf(expectedNotification))
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
}
@Test
@ -142,8 +149,10 @@ class NotificationFactoryTest {
val events = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_MESSAGE_EVENT))
val emptyRoom = mapOf(A_ROOM_ID to events)
val fakeImageLoader = FakeImageLoader()
val result = emptyRoom.toNotifications(
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
imageLoader = fakeImageLoader.getImageLoader(),
)
assertThat(result).isEqualTo(
@ -153,14 +162,17 @@ class NotificationFactoryTest {
)
)
)
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
}
@Test
fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) {
val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true))))
val fakeImageLoader = FakeImageLoader()
val result = redactedRoom.toNotifications(
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
imageLoader = fakeImageLoader.getImageLoader(),
)
assertThat(result).isEqualTo(
@ -170,6 +182,7 @@ class NotificationFactoryTest {
)
)
)
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
}
@Test
@ -189,11 +202,14 @@ class NotificationFactoryTest {
A_ROOM_ID,
)
val fakeImageLoader = FakeImageLoader()
val result = roomWithRedactedMessage.toNotifications(
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
imageLoader = fakeImageLoader.getImageLoader(),
)
assertThat(result).isEqualTo(listOf(expectedNotification))
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
}
}

View file

@ -21,12 +21,15 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationFactory
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
private const val MY_USER_DISPLAY_NAME = "display-name"
private const val MY_USER_AVATAR_URL = "avatar-url"
@ -42,6 +45,7 @@ private val MESSAGE_META = RoomNotification.Message.Meta(
)
private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1)
@RunWith(RobolectricTestRunner::class)
class NotificationRendererTest {
private val notificationDisplayer = FakeNotificationDisplayer()
@ -197,7 +201,8 @@ class NotificationRendererTest {
notificationRenderer.render(
MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT,
eventsToProcess = AN_EVENT_LIST
eventsToProcess = AN_EVENT_LIST,
imageLoader = FakeImageLoader().getImageLoader(),
)
}

View file

@ -17,19 +17,15 @@
package io.element.android.libraries.push.impl.notifications
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Build
import coil.Coil
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import coil.test.FakeImageLoaderEngine
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
@ -50,6 +46,7 @@ class RoomGroupMessageCreatorTest {
@Test
fun `test createRoomMessage with one Event`() = runTest {
val sut = createRoomGroupMessageCreator()
val fakeImageLoader = FakeImageLoader()
val result = sut.createRoomMessage(
currentUser = aMatrixUser(),
events = listOf(
@ -58,6 +55,7 @@ class RoomGroupMessageCreatorTest {
)
),
roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(),
)
val resultMetaWithoutFormatting = result.meta.copy(
summaryLine = result.meta.summaryLine.toString()
@ -71,11 +69,13 @@ class RoomGroupMessageCreatorTest {
shouldBing = false,
)
)
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
}
@Test
fun `test createRoomMessage with one noisy Event`() = runTest {
val sut = createRoomGroupMessageCreator()
val fakeImageLoader = FakeImageLoader()
val result = sut.createRoomMessage(
currentUser = aMatrixUser(),
events = listOf(
@ -84,6 +84,7 @@ class RoomGroupMessageCreatorTest {
)
),
roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(),
)
val resultMetaWithoutFormatting = result.meta.copy(
summaryLine = result.meta.summaryLine.toString()
@ -97,6 +98,7 @@ class RoomGroupMessageCreatorTest {
shouldBing = true,
)
)
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
}
@Test
@ -141,20 +143,7 @@ class RoomGroupMessageCreatorTest {
api: Int,
expectedCoilRequests: List<Any>,
) = runTest {
val coilRequests = mutableListOf<Any>()
val engine = FakeImageLoaderEngine.Builder()
.intercept(
predicate = {
coilRequests.add(it)
true
},
drawable = ColorDrawable(Color.BLUE)
)
.build()
val imageLoader = ImageLoader.Builder(RuntimeEnvironment.getApplication())
.components { add(engine) }
.build()
Coil.setImageLoader(imageLoader)
val fakeImageLoader = FakeImageLoader()
val sut = createRoomGroupMessageCreator(
sdkIntProvider = FakeBuildVersionSdkIntProvider(api)
)
@ -170,6 +159,7 @@ class RoomGroupMessageCreatorTest {
)
),
roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(),
)
val resultMetaWithoutFormatting = result.meta.copy(
summaryLine = result.meta.summaryLine.toString()
@ -183,12 +173,13 @@ class RoomGroupMessageCreatorTest {
shouldBing = false,
)
)
assertThat(coilRequests.toList()).isEqualTo(expectedCoilRequests)
assertThat(fakeImageLoader.getCoilRequests()).isEqualTo(expectedCoilRequests)
}
@Test
fun `test createRoomMessage with two Events`() = runTest {
val sut = createRoomGroupMessageCreator()
val fakeImageLoader = FakeImageLoader()
val result = sut.createRoomMessage(
currentUser = aMatrixUser(),
events = listOf(
@ -196,6 +187,7 @@ class RoomGroupMessageCreatorTest {
aNotifiableMessageEvent(timestamp = A_TIMESTAMP + 10),
),
roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(),
)
val resultMetaWithoutFormatting = result.meta.copy(
summaryLine = result.meta.summaryLine.toString()
@ -209,11 +201,13 @@ class RoomGroupMessageCreatorTest {
shouldBing = false,
)
)
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
}
@Test
fun `test createRoomMessage with smart reply error`() = runTest {
val sut = createRoomGroupMessageCreator()
val fakeImageLoader = FakeImageLoader()
val result = sut.createRoomMessage(
currentUser = aMatrixUser(),
events = listOf(
@ -223,6 +217,7 @@ class RoomGroupMessageCreatorTest {
),
),
roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(),
)
val resultMetaWithoutFormatting = result.meta.copy(
summaryLine = result.meta.summaryLine.toString()
@ -236,11 +231,13 @@ class RoomGroupMessageCreatorTest {
shouldBing = false,
)
)
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
}
@Test
fun `test createRoomMessage for direct room`() = runTest {
val sut = createRoomGroupMessageCreator()
val fakeImageLoader = FakeImageLoader()
val result = sut.createRoomMessage(
currentUser = aMatrixUser(),
events = listOf(
@ -249,6 +246,7 @@ class RoomGroupMessageCreatorTest {
),
),
roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(),
)
val resultMetaWithoutFormatting = result.meta.copy(
summaryLine = result.meta.summaryLine.toString()
@ -262,6 +260,7 @@ class RoomGroupMessageCreatorTest {
shouldBing = false,
)
)
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications.fake
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import coil.test.FakeImageLoaderEngine
import org.robolectric.RuntimeEnvironment
@OptIn(ExperimentalCoilApi::class)
class FakeImageLoader {
private val coilRequests = mutableListOf<Any>()
private var cache: ImageLoader? = null
fun getImageLoader(): ImageLoader {
return cache ?: ImageLoader.Builder(RuntimeEnvironment.getApplication())
.components {
val engine = FakeImageLoaderEngine.Builder()
.intercept(
predicate = {
coilRequests.add(it)
true
},
drawable = ColorDrawable(Color.BLUE)
)
.build()
add(engine)
}
.build()
.also {
cache = it
}
}
fun getCoilRequests(): List<Any> {
return coilRequests.toList()
}
}

View file

@ -14,13 +14,15 @@
* limitations under the License.
*/
package io.element.android.libraries.matrix.ui.di
package io.element.android.libraries.push.impl.notifications.fake
import com.squareup.anvil.annotations.ContributesTo
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.ui.media.LoggedInImageLoaderFactory
import coil.ImageLoader
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
@ContributesTo(SessionScope::class)
interface MatrixUIBindings {
fun loggedInImageLoaderFactory(): LoggedInImageLoaderFactory
class FakeImageLoaderHolder : ImageLoaderHolder {
private val fakeImageLoader = FakeImageLoader()
override fun get(client: MatrixClient): ImageLoader {
return fakeImageLoader.getImageLoader()
}
}

View file

@ -40,7 +40,7 @@ class FakeNotificationFactory {
summaryNotification: SummaryNotification
) {
with(instance) {
coEvery { groupedEvents.roomEvents.toNotifications(matrixUser) } returns roomNotifications
coEvery { groupedEvents.roomEvents.toNotifications(matrixUser, any()) } returns roomNotifications
every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications
every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications
every { groupedEvents.fallbackEvents.toNotifications() } returns fallbackNotifications

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications.fake
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.push.impl.notifications.NotificationMediaRepo
import java.io.File
class FakeNotificationMediaRepo : NotificationMediaRepo {
override suspend fun getMediaFile(
mediaSource: MediaSource,
mimeType: String?,
body: String?,
): Result<File> {
return Result.failure(IllegalStateException("Fake class"))
}
}

View file

@ -39,6 +39,7 @@ class FakeRoomGroupMessageCreator {
currentUser = matrixUser,
events = events,
roomId = roomId,
imageLoader = any(),
)
} returns mockMessage
return mockMessage

View file

@ -44,6 +44,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -551,7 +552,7 @@ private fun ReplyToModeView(
) {
Text(
text = senderName,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().clipToBounds(),
style = ElementTheme.typography.fontBodySmMedium,
textAlign = TextAlign.Start,
color = ElementTheme.materialColors.primary,

View file

@ -21,4 +21,5 @@
<string name="rich_text_editor_unindent">"Ohne Einrückung"</string>
<string name="rich_text_editor_url_placeholder">"Link"</string>
<string name="rich_text_editor_a11y_add_attachment">"Anhang hinzufügen"</string>
<string name="screen_room_voice_message_tooltip">"Zum Aufnehmen gedrückt halten"</string>
</resources>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="rich_text_editor_bullet_list">"Felsorolás be/ki"</string>
<string name="rich_text_editor_close_formatting_options">"Formázási beállítások bezárása"</string>
<string name="rich_text_editor_code_block">"Kódblokk be/ki"</string>
<string name="rich_text_editor_composer_placeholder">"Üzenet…"</string>
<string name="rich_text_editor_create_link">"Hivatkozás létrehozása"</string>
<string name="rich_text_editor_edit_link">"Hivatkozás szerkesztése"</string>
<string name="rich_text_editor_format_bold">"Félkövér formátum alkalmazása"</string>
<string name="rich_text_editor_format_italic">"Dőlt formátum alkalmazása"</string>
<string name="rich_text_editor_format_strikethrough">"Áthúzott formátum alkalmazása"</string>
<string name="rich_text_editor_format_underline">"Aláhúzott formátum alkalmazása"</string>
<string name="rich_text_editor_full_screen_toggle">"Teljes képernyős mód be/ki"</string>
<string name="rich_text_editor_indent">"Behúzás"</string>
<string name="rich_text_editor_inline_code">"Soron belüli kód formátum alkalmazása"</string>
<string name="rich_text_editor_link">"Hivatkozás beállítása"</string>
<string name="rich_text_editor_numbered_list">"Számozott lista be/ki"</string>
<string name="rich_text_editor_open_compose_options">"Írási beállítások megnyitása"</string>
<string name="rich_text_editor_quote">"Idézet be/ki"</string>
<string name="rich_text_editor_remove_link">"Hivatkozás eltávolítása"</string>
<string name="rich_text_editor_unindent">"Behúzás nélkül"</string>
<string name="rich_text_editor_url_placeholder">"Hivatkozás"</string>
<string name="rich_text_editor_a11y_add_attachment">"Melléklet hozzáadása"</string>
<string name="screen_room_voice_message_tooltip">"Tartsa a rögzítéshez"</string>
</resources>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="rich_text_editor_bullet_list">"Alihkan daftar poin"</string>
<string name="rich_text_editor_close_formatting_options">"Tutup opsi pemformatan"</string>
<string name="rich_text_editor_code_block">"Alihkan blok kode"</string>
<string name="rich_text_editor_composer_placeholder">"Kirim pesan…"</string>
<string name="rich_text_editor_create_link">"Buat tautan"</string>
<string name="rich_text_editor_edit_link">"Sunting tautan"</string>
<string name="rich_text_editor_format_bold">"Terapkan format tebal"</string>
<string name="rich_text_editor_format_italic">"Terapkan format miring"</string>
<string name="rich_text_editor_format_strikethrough">"Terapkan format coret"</string>
<string name="rich_text_editor_format_underline">"Terapkan format garis bawah"</string>
<string name="rich_text_editor_full_screen_toggle">"Alihkan mode layar penuh"</string>
<string name="rich_text_editor_indent">"Beri indentasi"</string>
<string name="rich_text_editor_inline_code">"Terapkan format kode dalam baris"</string>
<string name="rich_text_editor_link">"Tetapkan tautan"</string>
<string name="rich_text_editor_numbered_list">"Alihkan daftar bernomor"</string>
<string name="rich_text_editor_open_compose_options">"Buka opsi penulisan"</string>
<string name="rich_text_editor_quote">"Alihkan kutipan"</string>
<string name="rich_text_editor_remove_link">"Hapus tautan"</string>
<string name="rich_text_editor_unindent">"Hapus indentasi"</string>
<string name="rich_text_editor_url_placeholder">"Tautan"</string>
<string name="rich_text_editor_a11y_add_attachment">"Tambahkan lampiran"</string>
<string name="screen_room_voice_message_tooltip">"Tahan untuk merekam"</string>
</resources>

View file

@ -109,6 +109,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_direct_chat">"Přímý chat"</string>
<string name="common_edited_suffix">"(upraveno)"</string>
<string name="common_editing">"Úpravy"</string>
<string name="common_emote">"* %1$s %2$s"</string>
@ -130,7 +131,7 @@
<string name="common_loading">"Načítání…"</string>
<string name="common_message">"Zpráva"</string>
<string name="common_message_actions">"Akce zprávy"</string>
<string name="common_message_layout">"Rozložení zprávy"</string>
<string name="common_message_layout">"Zobrazení zpráv"</string>
<string name="common_message_removed">"Zpráva byla odstraněna"</string>
<string name="common_modern">"Moderní"</string>
<string name="common_mute">"Ztlumit"</string>

View file

@ -1,13 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_delete">"Löschen"</string>
<string name="a11y_hide_password">"Passwort verbergen"</string>
<string name="a11y_jump_to_bottom">"Nach unten springen"</string>
<string name="a11y_notifications_mentions_only">"Nur Erwähnungen"</string>
<string name="a11y_notifications_muted">"Stummgeschaltet"</string>
<string name="a11y_page_n">"Seite %1$d"</string>
<string name="a11y_pause">"Pausieren"</string>
<string name="a11y_pin_field">"PIN-Feld"</string>
<string name="a11y_play">"Abspielen"</string>
<string name="a11y_poll">"Umfrage"</string>
<string name="a11y_poll_end">"Umfrage beendet"</string>
<string name="a11y_react_with">"Reagiere mit %1$s"</string>
<string name="a11y_react_with_other_emojis">"Mit anderen Emojis reagieren"</string>
<string name="a11y_read_receipts_multiple">"Gelesen von %1$s und %2$s"</string>
<string name="a11y_read_receipts_single">"Gelesen von %1$s"</string>
<string name="a11y_read_receipts_tap_to_show_all">"Tippe, um alle anzuzeigen"</string>
<string name="a11y_remove_reaction_with">"Reaktion mit %1$s entfernen"</string>
<string name="a11y_send_files">"Dateien senden"</string>
<string name="a11y_show_password">"Passwort anzeigen"</string>
<string name="a11y_start_call">"Anruf starten"</string>
<string name="a11y_user_menu">"Benutzermenü"</string>
<string name="a11y_voice_message_record">"Sprachnachricht aufnehmen."</string>
<string name="a11y_voice_message_stop_recording">"Aufnahme beenden"</string>
<string name="action_accept">"Akzeptieren"</string>
<string name="action_add_to_timeline">"Zur Zeitleiste hinzufügen"</string>
<string name="action_back">"Zurück"</string>
@ -24,6 +39,7 @@
<string name="action_create">"Erstellen"</string>
<string name="action_create_a_room">"Raum erstellen"</string>
<string name="action_decline">"Ablehnen"</string>
<string name="action_delete_poll">"Umfrage löschen"</string>
<string name="action_disable">"Deaktivieren"</string>
<string name="action_done">"Erledigt"</string>
<string name="action_edit">"Bearbeiten"</string>
@ -66,23 +82,31 @@
<string name="action_send_message">"Nachricht senden"</string>
<string name="action_share">"Teilen"</string>
<string name="action_share_link">"Link teilen"</string>
<string name="action_sign_in_again">"Erneut anmelden"</string>
<string name="action_signout">"Abmelden"</string>
<string name="action_signout_anyway">"Trotzdem abmelden"</string>
<string name="action_skip">"Überspringen"</string>
<string name="action_start">"Start"</string>
<string name="action_start_chat">"Chat starten"</string>
<string name="action_start_verification">"Verifizierung starten"</string>
<string name="action_static_map_load">"Tippe, um die Karte zu laden"</string>
<string name="action_take_photo">"Foto machen"</string>
<string name="action_tap_for_options">"Für Optionen tippen"</string>
<string name="action_try_again">"Erneut versuchen"</string>
<string name="action_view_source">"Quelle anzeigen"</string>
<string name="action_yes">"Ja"</string>
<string name="common_about">"Über"</string>
<string name="common_acceptable_use_policy">"Nutzungsrichtlinie"</string>
<string name="common_advanced_settings">"Erweiterte Einstellungen"</string>
<string name="common_analytics">"Analysedaten"</string>
<string name="common_appearance">"Erscheinungsbild"</string>
<string name="common_audio">"Audio"</string>
<string name="common_bubbles">"Blasen"</string>
<string name="common_chat_backup">"Chat-Backup"</string>
<string name="common_copyright">"Copyright"</string>
<string name="common_creating_room">"Raum wird erstellt…"</string>
<string name="common_current_user_left_room">"Raum verlassen"</string>
<string name="common_dark">"Dunkel"</string>
<string name="common_decryption_error">"Dekodierungsfehler"</string>
<string name="common_developer_options">"Entwickleroptionen"</string>
<string name="common_edited_suffix">"(bearbeitet)"</string>
@ -91,6 +115,7 @@
<string name="common_encryption_enabled">"Verschlüsselung aktiviert"</string>
<string name="common_enter_your_pin">"PIN eingeben"</string>
<string name="common_error">"Fehler"</string>
<string name="common_everyone">"Alle"</string>
<string name="common_file">"Datei"</string>
<string name="common_file_saved_on_disk_android">"Datei wurde unter Downloads gespeichert"</string>
<string name="common_forward_message">"Nachricht weiterleiten"</string>
@ -100,9 +125,11 @@
<string name="common_install_apk_android">"APK installieren"</string>
<string name="common_invite_unknown_profile">"Diese Matrix-ID kann nicht gefunden werden, daher wird die Einladung möglicherweise nicht empfangen."</string>
<string name="common_leaving_room">"Raum verlassen"</string>
<string name="common_light">"Hell"</string>
<string name="common_link_copied_to_clipboard">"Link in die Zwischenablage kopiert"</string>
<string name="common_loading">"Laden…"</string>
<string name="common_message">"Nachricht"</string>
<string name="common_message_actions">"Nachrichtenaktionen"</string>
<string name="common_message_layout">"Nachrichtenlayout"</string>
<string name="common_message_removed">"Nachricht entfernt"</string>
<string name="common_modern">"Modern"</string>
@ -121,23 +148,31 @@
<string name="common_refreshing">"Wird erneuert…"</string>
<string name="common_replying_to">"%1$s antworten"</string>
<string name="common_report_a_bug">"Einen Fehler melden"</string>
<string name="common_report_a_problem">"Ein Problem melden"</string>
<string name="common_report_submitted">"Bericht eingereicht"</string>
<string name="common_rich_text_editor">"Rich-Text-Editor"</string>
<string name="common_room">"Raum"</string>
<string name="common_room_name">"Raumname"</string>
<string name="common_room_name_placeholder">"z.B. dein Projektname"</string>
<string name="common_screen_lock">"Bildschirmsperre"</string>
<string name="common_search_for_someone">"Nach jemandem suchen"</string>
<string name="common_search_results">"Suchergebnisse"</string>
<string name="common_security">"Sicherheit"</string>
<string name="common_seen_by">"Gesehen von"</string>
<string name="common_sending">"Wird gesendet…"</string>
<string name="common_sending_failed">"Senden fehlgeschlagen"</string>
<string name="common_sent">"Gesendet"</string>
<string name="common_server_not_supported">"Server wird nicht unterstützt"</string>
<string name="common_server_url">"Server-URL"</string>
<string name="common_settings">"Einstellungen"</string>
<string name="common_shared_location">"Geteilter Standort"</string>
<string name="common_signing_out">"Abmelden"</string>
<string name="common_starting_chat">"Chat wird gestartet…"</string>
<string name="common_sticker">"Sticker"</string>
<string name="common_success">"Erfolg"</string>
<string name="common_suggestions">"Vorschläge"</string>
<string name="common_syncing">"Synchronisieren"</string>
<string name="common_system">"System"</string>
<string name="common_text">"Text"</string>
<string name="common_third_party_notices">"Hinweise von Drittanbietern"</string>
<string name="common_thread">"Thread"</string>
@ -146,28 +181,42 @@
<string name="common_unable_to_decrypt">"Entschlüsselung nicht möglich"</string>
<string name="common_unable_to_invite_message">"Einladungen konnten nicht an einen oder mehrere Benutzer gesendet werden."</string>
<string name="common_unable_to_invite_title">"Einladung(en) konnte(n) nicht gesendet werden"</string>
<string name="common_unlock">"Entsperren"</string>
<string name="common_unmute">"Stummschaltung aufheben"</string>
<string name="common_unsupported_event">"Nicht unterstütztes Ereignis"</string>
<string name="common_username">"Benutzername"</string>
<string name="common_verification_cancelled">"Verifizierung abgebrochen"</string>
<string name="common_verification_complete">"Verifizierung abgeschlossen"</string>
<string name="common_video">"Video"</string>
<string name="common_voice_message">"Sprachnachricht"</string>
<string name="common_waiting">"Warten…"</string>
<string name="common_waiting_for_decryption_key">"Warte auf diese Nachricht"</string>
<string name="common_poll_end_confirmation">"Bist du sicher, dass du diese Umfrage beenden möchtest?"</string>
<string name="common_poll_summary">"Umfrage: %1$s"</string>
<string name="common_verify_device">"Gerät verifizieren"</string>
<string name="dialog_title_confirmation">"Bestätigung"</string>
<string name="dialog_title_warning">"Warnung"</string>
<string name="error_failed_creating_the_permalink">"Fehler beim Erstellen des Permalinks"</string>
<string name="error_failed_loading_map">"%1$s konnte die Karte nicht laden. Bitte versuche es später erneut."</string>
<string name="error_failed_loading_messages">"Fehler beim Laden der Nachrichten"</string>
<string name="error_failed_locating_user">"%1$s konnte nicht auf deinen Standort zugreifen. Bitte versuche es später erneut."</string>
<string name="error_failed_uploading_voice_message">"Fehler beim Hochladen der Sprachnachricht."</string>
<string name="error_missing_location_auth_android">"%1$s hat keine Erlaubnis, auf deinen Standort zuzugreifen. Du kannst den Zugriff in den Einstellungen aktivieren."</string>
<string name="error_missing_location_rationale_android">"%1$s hat keine Erlaubnis, auf deinen Standort zuzugreifen. Aktiviere unten den Zugriff."</string>
<string name="error_missing_microphone_voice_rationale_android">"%1$s hat nicht die Erlaubnis auf dein Mikrofon zuzugreifen. Aktiviere den Zugriff, um eine Sprachnachricht aufzunehmen."</string>
<string name="error_some_messages_have_not_been_sent">"Einige Nachrichten wurden nicht gesendet"</string>
<string name="error_unknown">"Entschuldigung, es ist ein Fehler aufgetreten"</string>
<string name="invite_friends_rich_title">"🔐️ Begleite mich auf %1$s"</string>
<string name="invite_friends_text">"Hey, sprich mit mir auf %1$s: %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<plurals name="a11y_digits_entered">
<item quantity="one">"%1$d eingegebene Ziffer"</item>
<item quantity="other">"%1$d eingegebene Ziffern"</item>
</plurals>
<plurals name="a11y_read_receipts_multiple_with_others">
<item quantity="one">"Gelesen von %1$s und %2$d anderen"</item>
<item quantity="other">"Gelesen von %1$s und %2$d anderen"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d Mitglied"</item>
<item quantity="other">"%1$d Mitglieder"</item>

View file

@ -2,15 +2,21 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_delete">"Supprimer"</string>
<string name="a11y_hide_password">"Masquer le mot de passe"</string>
<string name="a11y_jump_to_bottom">"Retourner à la fin de la conversation"</string>
<string name="a11y_notifications_mentions_only">"Mentions uniquement"</string>
<string name="a11y_notifications_muted">"En sourdine"</string>
<string name="a11y_page_n">"Page %1$d"</string>
<string name="a11y_pause">"Pause"</string>
<string name="a11y_pin_field">"Code PIN"</string>
<string name="a11y_play">"Lecture"</string>
<string name="a11y_poll">"Sondage"</string>
<string name="a11y_poll_end">"Sondage terminé"</string>
<string name="a11y_react_with">"Réagir avec %1$s"</string>
<string name="a11y_react_with_other_emojis">"Réagir avec dautres emojis"</string>
<string name="a11y_read_receipts_multiple">"Lu par %1$s et %2$s"</string>
<string name="a11y_read_receipts_single">"Lu par %1$s"</string>
<string name="a11y_read_receipts_tap_to_show_all">"Taper pour voir toute la liste"</string>
<string name="a11y_remove_reaction_with">"Supprimer la réaction avec %1$s"</string>
<string name="a11y_send_files">"Envoyer des fichiers"</string>
<string name="a11y_show_password">"Afficher le mot de passe"</string>
<string name="a11y_start_call">"Démarrer un appel"</string>
@ -33,6 +39,7 @@
<string name="action_create">"Créer"</string>
<string name="action_create_a_room">"Créer un salon"</string>
<string name="action_decline">"Refuser"</string>
<string name="action_delete_poll">"Supprimer le sondage"</string>
<string name="action_disable">"Désactiver"</string>
<string name="action_done">"Terminé"</string>
<string name="action_edit">"Modifier"</string>
@ -88,6 +95,7 @@
<string name="action_try_again">"Essayer à nouveau"</string>
<string name="action_view_source">"Afficher la source"</string>
<string name="action_yes">"Oui"</string>
<string name="action_load_more">"Voir plus"</string>
<string name="common_about">"À propos"</string>
<string name="common_acceptable_use_policy">"Politique dutilisation acceptable"</string>
<string name="common_advanced_settings">"Paramètres avancés"</string>
@ -102,6 +110,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_direct_chat">"Discussion à deux"</string>
<string name="common_edited_suffix">"(modifié)"</string>
<string name="common_editing">"Édition"</string>
<string name="common_emote">"* %1$s %2$s"</string>

View file

@ -0,0 +1,237 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_delete">"Törlés"</string>
<string name="a11y_hide_password">"Jelszó elrejtése"</string>
<string name="a11y_jump_to_bottom">"Ugrás az aljára"</string>
<string name="a11y_notifications_mentions_only">"Csak megemlítések"</string>
<string name="a11y_notifications_muted">"Némítva"</string>
<string name="a11y_page_n">"%1$d. oldal"</string>
<string name="a11y_pause">"Szüneteltetés"</string>
<string name="a11y_pin_field">"PIN-mező"</string>
<string name="a11y_play">"Lejátszás"</string>
<string name="a11y_poll">"Szavazás"</string>
<string name="a11y_poll_end">"Szavazás befejezve"</string>
<string name="a11y_react_with">"Reagálás a következővel: %1$s"</string>
<string name="a11y_react_with_other_emojis">"Reagálás más emodzsikkal"</string>
<string name="a11y_read_receipts_multiple">"Olvasta: %1$s és %2$s"</string>
<string name="a11y_read_receipts_single">"Olvasta: %1$s"</string>
<string name="a11y_read_receipts_tap_to_show_all">"Koppintson az összes megjelenítéséhez"</string>
<string name="a11y_remove_reaction_with">"Reakció eltávolítása: %1$s"</string>
<string name="a11y_send_files">"Fájlküldés"</string>
<string name="a11y_show_password">"Jelszó megjelenítése"</string>
<string name="a11y_start_call">"Hanghívás indítása"</string>
<string name="a11y_user_menu">"Felhasználói menü"</string>
<string name="a11y_voice_message_record">"Hangüzenet felvétele."</string>
<string name="a11y_voice_message_stop_recording">"Rögzítés leállítása"</string>
<string name="action_accept">"Elfogadás"</string>
<string name="action_add_to_timeline">"Hozzáadás az idővonalhoz"</string>
<string name="action_back">"Vissza"</string>
<string name="action_cancel">"Mégse"</string>
<string name="action_choose_photo">"Fénykép kiválasztása"</string>
<string name="action_clear">"Törlés"</string>
<string name="action_close">"Bezárás"</string>
<string name="action_complete_verification">"Ellenőrzés befejezése"</string>
<string name="action_confirm">"Megerősítés"</string>
<string name="action_continue">"Folytatás"</string>
<string name="action_copy">"Másolás"</string>
<string name="action_copy_link">"Hivatkozás másolása"</string>
<string name="action_copy_link_to_message">"Üzenetre mutató hivatkozás másolása"</string>
<string name="action_create">"Létrehozás"</string>
<string name="action_create_a_room">"Szoba létrehozása"</string>
<string name="action_decline">"Elutasítás"</string>
<string name="action_delete_poll">"Szavazás törlése"</string>
<string name="action_disable">"Letiltás"</string>
<string name="action_done">"Kész"</string>
<string name="action_edit">"Szerkesztés"</string>
<string name="action_edit_poll">"Szavazás szerkesztése"</string>
<string name="action_enable">"Engedélyezés"</string>
<string name="action_end_poll">"Szavazás befejezése"</string>
<string name="action_enter_pin">"Adja meg a PIN-kódot"</string>
<string name="action_forgot_password">"Elfelejtette a jelszót?"</string>
<string name="action_forward">"Tovább"</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>
<string name="action_invite_people_to_app">"Emberek meghívása ide: %1$s"</string>
<string name="action_invites_list">"Meghívások"</string>
<string name="action_join">"Csatlakozás"</string>
<string name="action_learn_more">"További tudnivalók"</string>
<string name="action_leave">"Kilépés"</string>
<string name="action_leave_room">"Szoba elhagyása"</string>
<string name="action_manage_account">"Fiók kezelése"</string>
<string name="action_manage_devices">"Eszközök kezelése"</string>
<string name="action_next">"Következő"</string>
<string name="action_no">"Nem"</string>
<string name="action_not_now">"Most nem"</string>
<string name="action_ok">"Rendben"</string>
<string name="action_open_settings">"Beállítások megnyitása"</string>
<string name="action_open_with">"Megnyitás…"</string>
<string name="action_quick_reply">"Gyors válasz"</string>
<string name="action_quote">"Idézet"</string>
<string name="action_react">"Reakció"</string>
<string name="action_remove">"Eltávolítás"</string>
<string name="action_reply">"Válasz"</string>
<string name="action_reply_in_thread">"Válasz az üzenetszálban"</string>
<string name="action_report_bug">"Hiba jelentése"</string>
<string name="action_report_content">"Tartalom jelentése"</string>
<string name="action_retry">"Újra"</string>
<string name="action_retry_decryption">"Visszafejtés újbóli megpróbálása"</string>
<string name="action_save">"Mentés"</string>
<string name="action_search">"Keresés"</string>
<string name="action_send">"Küldés"</string>
<string name="action_send_message">"Üzenet küldése"</string>
<string name="action_share">"Megosztás"</string>
<string name="action_share_link">"Hivatkozás megosztása"</string>
<string name="action_sign_in_again">"Jelentkezzen be újra"</string>
<string name="action_signout">"Kijelentkezés"</string>
<string name="action_signout_anyway">"Kijelentkezés mindenképp"</string>
<string name="action_skip">"Kihagyás"</string>
<string name="action_start">"Indítás"</string>
<string name="action_start_chat">"Csevegés indítása"</string>
<string name="action_start_verification">"Ellenőrzés elindítása"</string>
<string name="action_static_map_load">"Koppintson a térkép betöltéséhez"</string>
<string name="action_take_photo">"Fénykép készítése"</string>
<string name="action_tap_for_options">"Koppintson a lehetőségekért"</string>
<string name="action_try_again">"Próbálja újra"</string>
<string name="action_view_source">"Forrás megtekintése"</string>
<string name="action_yes">"Igen"</string>
<string name="common_about">"Névjegy"</string>
<string name="common_acceptable_use_policy">"Elfogadható használatra vonatkozó szabályzat"</string>
<string name="common_advanced_settings">"Speciális beállítások"</string>
<string name="common_analytics">"Elemzések"</string>
<string name="common_appearance">"Megjelenítés"</string>
<string name="common_audio">"Hang"</string>
<string name="common_bubbles">"Buborékok"</string>
<string name="common_chat_backup">"Csevegés biztonsági mentése"</string>
<string name="common_copyright">"Szerzői jogok"</string>
<string name="common_creating_room">"Szoba létrehozása…"</string>
<string name="common_current_user_left_room">"Kilépett a szobából"</string>
<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_direct_chat">"Közvetlen csevegés"</string>
<string name="common_edited_suffix">"(szerkesztve)"</string>
<string name="common_editing">"Szerkesztés"</string>
<string name="common_emote">"* %1$s %2$s"</string>
<string name="common_encryption_enabled">"Titkosítás engedélyezve"</string>
<string name="common_enter_your_pin">"Adja meg a PIN-kódját"</string>
<string name="common_error">"Hiba"</string>
<string name="common_everyone">"Mindenki"</string>
<string name="common_file">"Fájl"</string>
<string name="common_file_saved_on_disk_android">"A fájl a Letöltések mappába mentve"</string>
<string name="common_forward_message">"Üzenet továbbítása"</string>
<string name="common_gif">"GIF"</string>
<string name="common_image">"Kép"</string>
<string name="common_in_reply_to">"Válaszként erre: %1$s"</string>
<string name="common_install_apk_android">"APK telepítése"</string>
<string name="common_invite_unknown_profile">"Ez a Matrix-azonosító nem található, ezért előfordulhat, hogy a meghívó nem érkezik meg."</string>
<string name="common_leaving_room">"Szoba elhagyása"</string>
<string name="common_light">"Világos"</string>
<string name="common_link_copied_to_clipboard">"Hivatkozás a vágólapra másolva"</string>
<string name="common_loading">"Betöltés…"</string>
<string name="common_message">"Üzenet"</string>
<string name="common_message_actions">"Üzenetműveletek"</string>
<string name="common_message_layout">"Üzenet elrendezése"</string>
<string name="common_message_removed">"Üzenet eltávolítva"</string>
<string name="common_modern">"Modern"</string>
<string name="common_mute">"Némítás"</string>
<string name="common_no_results">"Nincs találat"</string>
<string name="common_offline">"Kapcsolat nélkül"</string>
<string name="common_password">"Jelszó"</string>
<string name="common_people">"Emberek"</string>
<string name="common_permalink">"Állandó hivatkozás"</string>
<string name="common_permission">"Engedély"</string>
<string name="common_poll_total_votes">"Összes szavazat: %1$s"</string>
<string name="common_poll_undisclosed_text">"Az eredmények a szavazás befejezése után jelennek meg"</string>
<string name="common_privacy_policy">"Adatvédelmi nyilatkozat"</string>
<string name="common_reaction">"Reakció"</string>
<string name="common_reactions">"Reakciók"</string>
<string name="common_recovery_key">"Helyreállítási kulcs"</string>
<string name="common_refreshing">"Frissítés…"</string>
<string name="common_replying_to">"Válasz %1$s számára"</string>
<string name="common_report_a_bug">"Hiba jelentése"</string>
<string name="common_report_a_problem">"Probléma jelentése"</string>
<string name="common_report_submitted">"A jelentés elküldve"</string>
<string name="common_rich_text_editor">"Formázott szöveges szerkesztő"</string>
<string name="common_room">"Szoba"</string>
<string name="common_room_name">"Szoba neve"</string>
<string name="common_room_name_placeholder">"például a projekt neve"</string>
<string name="common_screen_lock">"Képernyőzár"</string>
<string name="common_search_for_someone">"Személy keresése"</string>
<string name="common_search_results">"Keresési találatok"</string>
<string name="common_security">"Biztonság"</string>
<string name="common_seen_by">"Látta:"</string>
<string name="common_sending">"Küldés…"</string>
<string name="common_sending_failed">"A küldés sikertelen"</string>
<string name="common_sent">"Elküldve"</string>
<string name="common_server_not_supported">"A kiszolgáló nem támogatott"</string>
<string name="common_server_url">"Kiszolgáló webcíme"</string>
<string name="common_settings">"Beállítások"</string>
<string name="common_shared_location">"Megosztott hely"</string>
<string name="common_signing_out">"Kijelentkezés"</string>
<string name="common_starting_chat">"Csevegés megkezdése…"</string>
<string name="common_sticker">"Matrica"</string>
<string name="common_success">"Sikeres"</string>
<string name="common_suggestions">"Javaslatok"</string>
<string name="common_syncing">"Szinkronizálás"</string>
<string name="common_system">"Rendszer"</string>
<string name="common_text">"Szöveg"</string>
<string name="common_third_party_notices">"Harmadik felek nyilatkozatai"</string>
<string name="common_thread">"Üzenetszál"</string>
<string name="common_topic">"Téma"</string>
<string name="common_topic_placeholder">"Miről szól ez a szoba?"</string>
<string name="common_unable_to_decrypt">"Nem lehet visszafejteni"</string>
<string name="common_unable_to_invite_message">"Nem sikerült meghívót küldeni egy vagy több felhasználónak."</string>
<string name="common_unable_to_invite_title">"Nem sikerült meghívót küldeni"</string>
<string name="common_unlock">"Feloldás"</string>
<string name="common_unmute">"Némítás feloldása"</string>
<string name="common_unsupported_event">"Nem támogatott esemény"</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_video">"Videó"</string>
<string name="common_voice_message">"Hangüzenet"</string>
<string name="common_waiting">"Várakozás…"</string>
<string name="common_waiting_for_decryption_key">"Várakozás a visszafejtési kulcsra"</string>
<string name="common_poll_end_confirmation">"Biztos, hogy befejezi ezt a szavazást?"</string>
<string name="common_poll_summary">"Szavazás: %1$s"</string>
<string name="common_verify_device">"Eszköz ellenőrzése"</string>
<string name="dialog_title_confirmation">"Megerősítés"</string>
<string name="dialog_title_warning">"Figyelmeztetés"</string>
<string name="error_failed_creating_the_permalink">"Nem sikerült létrehozni az állandó hivatkozást"</string>
<string name="error_failed_loading_map">"Az %1$s nem tudta betölteni a térképet. Próbálja meg újra később."</string>
<string name="error_failed_loading_messages">"Nem sikerült betölteni az üzeneteket"</string>
<string name="error_failed_locating_user">"Az %1$s nem tudta elérni a tartózkodási helyét. Próbálja meg újra később."</string>
<string name="error_failed_uploading_voice_message">"Nem sikerült feltölteni a hangüzenetét."</string>
<string name="error_missing_location_auth_android">"Az %1$snek nincs engedélye, hogy hozzáférjen a tartózkodási helyéhez. Ezt a beállításokban engedélyezheti."</string>
<string name="error_missing_location_rationale_android">"Az %1$snek nincs engedélye, hogy hozzáférjen a tartózkodási helyéhez. Engedélyezze alább az elérését."</string>
<string name="error_missing_microphone_voice_rationale_android">"Az %1$snek nincs engedélye, hogy hozzáférjen a mikrofonjához. Engedélyezze, hogy tudjon hangüzenetet felvenni."</string>
<string name="error_some_messages_have_not_been_sent">"Néhány üzenet nem került elküldésre"</string>
<string name="error_unknown">"Elnézést, hiba történt"</string>
<string name="invite_friends_rich_title">"🔐️ Csatlakozz hozzám itt: %1$s"</string>
<string name="invite_friends_text">"Beszélj velem az %1$s használatával: %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<plurals name="a11y_digits_entered">
<item quantity="one">"%1$d megadott számjegy"</item>
<item quantity="other">"%1$d megadott számjegy"</item>
</plurals>
<plurals name="a11y_read_receipts_multiple_with_others">
<item quantity="one">"Olvasta: %1$s és még %2$d felhasználó"</item>
<item quantity="other">"Olvasta: %1$s és még %2$d felhasználó"</item>
</plurals>
<string name="preference_rageshake">"Az eszköz rázása a hibajelentéshez"</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>
<string name="screen_share_location_title">"Hely megosztása"</string>
<string name="screen_share_my_location_action">"Saját hely megosztása"</string>
<string name="screen_share_open_apple_maps">"Megnyitás az Apple Mapsben"</string>
<string name="screen_share_open_google_maps">"Megnyitás a Google Mapsben"</string>
<string name="screen_share_open_osm_maps">"Megnyitás az OpenStreetMapen"</string>
<string name="screen_share_this_location_action">"E hely megosztása"</string>
<string name="screen_view_location_title">"Hely"</string>
<string name="settings_version_number">"Verzió: %1$s (%2$s)"</string>
<string name="test_language_identifier">"hu"</string>
<string name="dialog_title_error">"Hiba"</string>
<string name="dialog_title_success">"Sikeres"</string>
</resources>

View file

@ -0,0 +1,242 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_delete">"Hapus"</string>
<string name="a11y_hide_password">"Sembunyikan kata sandi"</string>
<string name="a11y_jump_to_bottom">"Lompat ke bawah"</string>
<string name="a11y_notifications_mentions_only">"Hanya sebutan"</string>
<string name="a11y_notifications_muted">"Dibisukan"</string>
<string name="a11y_page_n">"Halaman %1$d"</string>
<string name="a11y_pause">"Jeda"</string>
<string name="a11y_pin_field">"Kolom PIN"</string>
<string name="a11y_play">"Putar"</string>
<string name="a11y_poll">"Pemungutan suara"</string>
<string name="a11y_poll_end">"Pemungutan suara berakhir"</string>
<string name="a11y_react_with">"Bereaksi dengan %1$s"</string>
<string name="a11y_react_with_other_emojis">"Reaksi dengan emoji lain"</string>
<string name="a11y_read_receipts_multiple">"Dibaca oleh %1$s dan %2$s"</string>
<string name="a11y_read_receipts_single">"Dibaca oleh %1$s"</string>
<string name="a11y_read_receipts_tap_to_show_all">"Ketuk untuk melihat semua"</string>
<string name="a11y_remove_reaction_with">"Hapus reaksi dengan %1$s"</string>
<string name="a11y_send_files">"Kirim berkas"</string>
<string name="a11y_show_password">"Tampilkan kata sandi"</string>
<string name="a11y_start_call">"Mulai panggilan"</string>
<string name="a11y_user_menu">"Menu pengguna"</string>
<string name="a11y_voice_message_record">"Rekam pesan suara."</string>
<string name="a11y_voice_message_stop_recording">"Berhenti merekam"</string>
<string name="action_accept">"Terima"</string>
<string name="action_add_to_timeline">"Tambahkan ke lini masa"</string>
<string name="action_back">"Kembali"</string>
<string name="action_cancel">"Batal"</string>
<string name="action_choose_photo">"Pilih foto"</string>
<string name="action_clear">"Hapus"</string>
<string name="action_close">"Tutup"</string>
<string name="action_complete_verification">"Selesaikan verifikasi"</string>
<string name="action_confirm">"Konfirmasi"</string>
<string name="action_continue">"Lanjutkan"</string>
<string name="action_copy">"Salin"</string>
<string name="action_copy_link">"Salin tautan"</string>
<string name="action_copy_link_to_message">"Salin tautan ke pesan"</string>
<string name="action_create">"Buat"</string>
<string name="action_create_a_room">"Buat ruangan"</string>
<string name="action_decline">"Tolak"</string>
<string name="action_delete_poll">"Hapus pemungutan suara"</string>
<string name="action_disable">"Nonaktifkan"</string>
<string name="action_done">"Selesai"</string>
<string name="action_edit">"Sunting"</string>
<string name="action_edit_poll">"Sunting pemungutan suara"</string>
<string name="action_enable">"Aktifkan"</string>
<string name="action_end_poll">"Akhiri pemungutan suara"</string>
<string name="action_enter_pin">"Masukkan PIN"</string>
<string name="action_forgot_password">"Lupa kata sandi?"</string>
<string name="action_forward">"Teruskan"</string>
<string name="action_invite">"Undang"</string>
<string name="action_invite_friends">"Undang teman"</string>
<string name="action_invite_friends_to_app">"Undang teman ke %1$s"</string>
<string name="action_invite_people_to_app">"Undang orang-orang ke %1$s"</string>
<string name="action_invites_list">"Undangan"</string>
<string name="action_join">"Gabung"</string>
<string name="action_learn_more">"Pelajari lebih lanjut"</string>
<string name="action_leave">"Tinggalkan"</string>
<string name="action_leave_room">"Tinggalkan ruangan"</string>
<string name="action_manage_account">"Kelola akun"</string>
<string name="action_manage_devices">"Kelola perangkat"</string>
<string name="action_next">"Berikutnya"</string>
<string name="action_no">"Tidak"</string>
<string name="action_not_now">"Jangan sekarang"</string>
<string name="action_ok">"Oke"</string>
<string name="action_open_settings">"Pengaturan"</string>
<string name="action_open_with">"Buka dengan"</string>
<string name="action_quick_reply">"Balas cepat"</string>
<string name="action_quote">"Kutip"</string>
<string name="action_react">"Bereaksi"</string>
<string name="action_remove">"Hapus"</string>
<string name="action_reply">"Balas"</string>
<string name="action_reply_in_thread">"Balas dalam utas"</string>
<string name="action_report_bug">"Laporkan kutu"</string>
<string name="action_report_content">"Laporkan Konten"</string>
<string name="action_retry">"Coba lagi"</string>
<string name="action_retry_decryption">"Coba dekripsi ulang"</string>
<string name="action_save">"Simpan"</string>
<string name="action_search">"Cari"</string>
<string name="action_send">"Kirim"</string>
<string name="action_send_message">"Kirim pesan"</string>
<string name="action_share">"Bagikan"</string>
<string name="action_share_link">"Bagikan tautan"</string>
<string name="action_sign_in_again">"Masuk lagi"</string>
<string name="action_signout">"Keluar dari akun"</string>
<string name="action_signout_anyway">"Keluar saja"</string>
<string name="action_skip">"Lewati"</string>
<string name="action_start">"Mulai"</string>
<string name="action_start_chat">"Mulai obrolan"</string>
<string name="action_start_verification">"Mulai verifikasi"</string>
<string name="action_static_map_load">"Ketuk untuk memuat peta"</string>
<string name="action_take_photo">"Ambil foto"</string>
<string name="action_tap_for_options">"Ketuk untuk opsi"</string>
<string name="action_try_again">"Coba lagi"</string>
<string name="action_view_source">"Tampilkan sumber"</string>
<string name="action_yes">"Ya"</string>
<string name="action_load_more">"Muat lainnya"</string>
<string name="common_about">"Tentang"</string>
<string name="common_acceptable_use_policy">"Kebijakan penggunaan wajar"</string>
<string name="common_advanced_settings">"Pengaturan tingkat lanjut"</string>
<string name="common_analytics">"Analitik"</string>
<string name="common_appearance">"Penampilan"</string>
<string name="common_audio">"Audio"</string>
<string name="common_bubbles">"Gelembung"</string>
<string name="common_chat_backup">"Pencadangan percakapan"</string>
<string name="common_copyright">"Hak cipta"</string>
<string name="common_creating_room">"Membuat ruangan…"</string>
<string name="common_current_user_left_room">"Keluar dari ruangan"</string>
<string name="common_dark">"Gelap"</string>
<string name="common_decryption_error">"Kesalahan dekripsi"</string>
<string name="common_developer_options">"Opsi pengembang"</string>
<string name="common_direct_chat">"Obrolan langsung"</string>
<string name="common_edited_suffix">"(disunting)"</string>
<string name="common_editing">"Penyuntingan"</string>
<string name="common_emote">"* %1$s %2$s"</string>
<string name="common_encryption_enabled">"Enkripsi diaktifkan"</string>
<string name="common_enter_your_pin">"Masukkan PIN Anda"</string>
<string name="common_error">"Eror"</string>
<string name="common_everyone">"Semua orang"</string>
<string name="common_file">"Berkas"</string>
<string name="common_file_saved_on_disk_android">"Berkas disimpan ke Unduhan"</string>
<string name="common_forward_message">"Teruskan pesan"</string>
<string name="common_gif">"GIF"</string>
<string name="common_image">"Gambar"</string>
<string name="common_in_reply_to">"Membalas kepada %1$s"</string>
<string name="common_install_apk_android">"Pasang APK"</string>
<string name="common_invite_unknown_profile">"ID Matrix ini tidak dapat ditemukan, sehingga undangan mungkin tidak diterima."</string>
<string name="common_leaving_room">"Meninggalkan ruangan"</string>
<string name="common_light">"Terang"</string>
<string name="common_link_copied_to_clipboard">"Tautan disalin ke papan klip"</string>
<string name="common_loading">"Memuat…"</string>
<string name="common_message">"Pesan"</string>
<string name="common_message_actions">"Tindakan pesan"</string>
<string name="common_message_layout">"Tata letak pesan"</string>
<string name="common_message_removed">"Pesan dihapus"</string>
<string name="common_modern">"Modern"</string>
<string name="common_mute">"Bisukan"</string>
<string name="common_no_results">"Tidak ada hasil"</string>
<string name="common_offline">"Luring"</string>
<string name="common_password">"Kata sandi"</string>
<string name="common_people">"Orang"</string>
<string name="common_permalink">"Tautan Permanen"</string>
<string name="common_permission">"Perizinan"</string>
<string name="common_poll_total_votes">"Total suara: %1$s"</string>
<string name="common_poll_undisclosed_text">"Hasil akan terlihat setelah pemungutan suara berakhir"</string>
<string name="common_privacy_policy">"Kebijakan privasi"</string>
<string name="common_reaction">"Reaksi"</string>
<string name="common_reactions">"Reaksi"</string>
<string name="common_recovery_key">"Kunci pemulihan"</string>
<string name="common_refreshing">"Menyegarkan…"</string>
<string name="common_replying_to">"Membalas %1$s"</string>
<string name="common_report_a_bug">"Laporkan kutu"</string>
<string name="common_report_a_problem">"Laporkan masalah"</string>
<string name="common_report_submitted">"Laporan terkirim"</string>
<string name="common_rich_text_editor">"Penyunting teks kaya"</string>
<string name="common_room">"Ruangan"</string>
<string name="common_room_name">"Nama ruangan"</string>
<string name="common_room_name_placeholder">"misalnya, nama proyek Anda"</string>
<string name="common_screen_lock">"Layar kunci"</string>
<string name="common_search_for_someone">"Cari seseorang"</string>
<string name="common_search_results">"Hasil pencarian"</string>
<string name="common_security">"Keamanan"</string>
<string name="common_seen_by">"Dilihat oleh"</string>
<string name="common_sending">"Mengirim…"</string>
<string name="common_sending_failed">"Pengiriman gagal"</string>
<string name="common_sent">"Terkirim"</string>
<string name="common_server_not_supported">"Server tidak didukung"</string>
<string name="common_server_url">"URL Server"</string>
<string name="common_settings">"Pengaturan"</string>
<string name="common_shared_location">"Membagikan lokasi"</string>
<string name="common_signing_out">"Mengeluarkan dari akun"</string>
<string name="common_starting_chat">"Memulai obrolan…"</string>
<string name="common_sticker">"Stiker"</string>
<string name="common_success">"Berhasil"</string>
<string name="common_suggestions">"Saran"</string>
<string name="common_syncing">"Menyinkronkan"</string>
<string name="common_system">"Sistem"</string>
<string name="common_text">"Teks"</string>
<string name="common_third_party_notices">"Pemberitahuan pihak ketiga"</string>
<string name="common_thread">"Utas"</string>
<string name="common_topic">"Topik"</string>
<string name="common_topic_placeholder">"Tentang apa ruangan ini?"</string>
<string name="common_unable_to_decrypt">"Tidak dapat mendekripsi"</string>
<string name="common_unable_to_invite_message">"Undangan tidak dapat dikirim ke satu atau beberapa pengguna."</string>
<string name="common_unable_to_invite_title">"Tidak dapat mengirim undangan"</string>
<string name="common_unlock">"Buka kunci"</string>
<string name="common_unmute">"Bunyikan"</string>
<string name="common_unsupported_event">"Peristiwa tidak didukung"</string>
<string name="common_username">"Nama pengguna"</string>
<string name="common_verification_cancelled">"Verifikasi dibatalkan"</string>
<string name="common_verification_complete">"Verifikasi selesai"</string>
<string name="common_video">"Video"</string>
<string name="common_voice_message">"Pesan suara"</string>
<string name="common_waiting">"Menunggu…"</string>
<string name="common_waiting_for_decryption_key">"Menunggu pesan ini"</string>
<string name="common_poll_end_confirmation">"Apakah Anda yakin ingin mengakhiri pemungutan suara ini?"</string>
<string name="common_poll_summary">"Pemungutan suara: %1$s"</string>
<string name="common_verify_device">"Verifikasi perangkat"</string>
<string name="dialog_title_confirmation">"Konfirmasi"</string>
<string name="dialog_title_warning">"Peringatan"</string>
<string name="error_failed_creating_the_permalink">"Gagal membuat tautan permanen"</string>
<string name="error_failed_loading_map">"%1$s tidak dapat memuat peta. Silakan coba lagi nanti."</string>
<string name="error_failed_loading_messages">"Gagal memuat pesan"</string>
<string name="error_failed_locating_user">"%1$s tidak dapat mengakses lokasi Anda. Silakan coba lagi nanti."</string>
<string name="error_failed_uploading_voice_message">"Gagal mengunggah pesan suara Anda."</string>
<string name="error_missing_location_auth_android">"%1$s tidak memiliki izin untuk mengakses lokasi Anda. Anda dapat mengaktifkan akses di Pengaturan."</string>
<string name="error_missing_location_rationale_android">"%1$s tidak memiliki izin untuk mengakses lokasi Anda. Aktifkan akses di bawah ini."</string>
<string name="error_missing_microphone_voice_rationale_android">"%1$s tidak memiliki izin untuk mengakses mikrofon. Aktifkan akses untuk merekam pesan suara."</string>
<string name="error_some_messages_have_not_been_sent">"Beberapa pesan belum terkirim"</string>
<string name="error_unknown">"Maaf, terjadi kesalahan"</string>
<string name="invite_friends_rich_title">"🔐️ Bergabunglah dengan saya di %1$s"</string>
<string name="invite_friends_text">"Hai, bicaralah dengan saya di %1$s: %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string>
<plurals name="a11y_digits_entered">
<item quantity="other">"%1$d digit dimasukkan"</item>
</plurals>
<plurals name="a11y_read_receipts_multiple_with_others">
<item quantity="other">"Dibaca oleh %1$s dan %2$d lainnya"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="other">"%1$d anggota"</item>
</plurals>
<plurals name="common_poll_votes_count">
<item quantity="other">"%d suara"</item>
</plurals>
<string name="preference_rageshake">"Rageshake untuk melaporkan kutu"</string>
<string name="screen_media_picker_error_failed_selection">"Gagal memilih media, silakan coba lagi."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Gagal memproses media untuk diunggah, silakan coba lagi."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Gagal mengunggah media, silakan coba lagi."</string>
<string name="screen_share_location_title">"Bagikan lokasi"</string>
<string name="screen_share_my_location_action">"Bagikan lokasi saya"</string>
<string name="screen_share_open_apple_maps">"Buka di Apple Maps"</string>
<string name="screen_share_open_google_maps">"Buka di Google Maps"</string>
<string name="screen_share_open_osm_maps">"Buka di OpenStreetMap"</string>
<string name="screen_share_this_location_action">"Bagikan lokasi ini"</string>
<string name="screen_view_location_title">"Lokasi"</string>
<string name="settings_version_number">"Versi: %1$s (%2$s)"</string>
<string name="test_language_identifier">"id"</string>
<string name="dialog_title_error">"Eror"</string>
<string name="dialog_title_success">"Berhasil"</string>
</resources>

View file

@ -95,6 +95,7 @@
<string name="action_try_again">"Повторить попытку"</string>
<string name="action_view_source">"Показать источник"</string>
<string name="action_yes">"Да"</string>
<string name="action_load_more">"Загрузить еще"</string>
<string name="common_about">"О приложении"</string>
<string name="common_acceptable_use_policy">"Политика допустимого использования"</string>
<string name="common_advanced_settings">"Дополнительные параметры"</string>
@ -109,6 +110,7 @@
<string name="common_dark">"Темная"</string>
<string name="common_decryption_error">"Ошибка расшифровки"</string>
<string name="common_developer_options">"Для разработчика"</string>
<string name="common_direct_chat">"Прямой чат"</string>
<string name="common_edited_suffix">"(изменено)"</string>
<string name="common_editing">"Редактирование"</string>
<string name="common_emote">"%1$s%2$s"</string>
@ -130,7 +132,7 @@
<string name="common_loading">"Загрузка…"</string>
<string name="common_message">"Сообщение"</string>
<string name="common_message_actions">"Действия с сообщением"</string>
<string name="common_message_layout">"Оформление сообщений"</string>
<string name="common_message_layout">"Оформление сообщения"</string>
<string name="common_message_removed">"Сообщение удалено"</string>
<string name="common_modern">"Современный"</string>
<string name="common_mute">"Без звука"</string>

View file

@ -95,6 +95,7 @@
<string name="action_try_again">"Skúste to znova"</string>
<string name="action_view_source">"Zobraziť zdroj"</string>
<string name="action_yes">"Áno"</string>
<string name="action_load_more">"Načítať viac"</string>
<string name="common_about">"O aplikácii"</string>
<string name="common_acceptable_use_policy">"Zásady prijateľného používania"</string>
<string name="common_advanced_settings">"Pokročilé nastavenia"</string>
@ -109,6 +110,7 @@
<string name="common_dark">"Tmavý"</string>
<string name="common_decryption_error">"Chyba dešifrovania"</string>
<string name="common_developer_options">"Možnosti pre vývojárov"</string>
<string name="common_direct_chat">"Priama konverzácia"</string>
<string name="common_edited_suffix">"(upravené)"</string>
<string name="common_editing">"Upravuje sa"</string>
<string name="common_emote">"* %1$s %2$s"</string>

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