Merge branch 'develop' into julioromano/poll_history_entry_point
This commit is contained in:
commit
863d156e4d
738 changed files with 9387 additions and 1581 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6831610b21668c49e31732f9005177e959277233d3cab758910e061294f91d79
|
||||
size 687979
|
||||
3
libraries/mediaupload/impl/src/test/assets/image.png
Normal file
3
libraries/mediaupload/impl/src/test/assets/image.png
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a980f7b74cb9edc323919db8652798da4b3dcf865fc7b6a1eb1110096b7bfb4f
|
||||
size 1856786
|
||||
3
libraries/mediaupload/impl/src/test/assets/sample3s.mp3
Normal file
3
libraries/mediaupload/impl/src/test/assets/sample3s.mp3
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0244590f2b4bcb62352b574e78bea940e8d89cfa69823b5208ef4c43e0abcb44
|
||||
size 52079
|
||||
3
libraries/mediaupload/impl/src/test/assets/text.txt
Normal file
3
libraries/mediaupload/impl/src/test/assets/text.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0ba904eae8773b70c75333db4de2f3ac45a8ad4ddba1b242f0b3cfc199391dd8
|
||||
size 13
|
||||
3
libraries/mediaupload/impl/src/test/assets/video.mp4
Normal file
3
libraries/mediaupload/impl/src/test/assets/video.mp4
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fb58436524db95bd0c10b2c3023c2eb7b87404a2eab8987939f051647eb859d3
|
||||
size 1673712
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
29
libraries/push/impl/src/main/res/values-hu/translations.xml
Normal file
29
libraries/push/impl/src/main/res/values-hu/translations.xml
Normal 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>
|
||||
47
libraries/push/impl/src/main/res/values-in/translations.xml
Normal file
47
libraries/push/impl/src/main/res/values-in/translations.xml
Normal 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>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path
|
||||
name="downloads"
|
||||
path="/" />
|
||||
</paths>
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ class FakeRoomGroupMessageCreator {
|
|||
currentUser = matrixUser,
|
||||
events = events,
|
||||
roomId = roomId,
|
||||
imageLoader = any(),
|
||||
)
|
||||
} returns mockMessage
|
||||
return mockMessage
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 d’autres 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 d’utilisation 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>
|
||||
|
|
|
|||
237
libraries/ui-strings/src/main/res/values-hu/translations.xml
Normal file
237
libraries/ui-strings/src/main/res/values-hu/translations.xml
Normal 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>
|
||||
242
libraries/ui-strings/src/main/res/values-in/translations.xml
Normal file
242
libraries/ui-strings/src/main/res/values-in/translations.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue