Merge branch 'develop' into feature/fga/message_queuing
This commit is contained in:
commit
b927daffe7
620 changed files with 6821 additions and 1244 deletions
|
|
@ -16,10 +16,22 @@
|
|||
|
||||
package io.element.android.libraries.androidutils.compat
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.os.Build
|
||||
|
||||
fun PackageManager.queryIntentActivitiesCompat(data: Intent, flags: Int): List<ResolveInfo> {
|
||||
return when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> queryIntentActivities(
|
||||
data,
|
||||
PackageManager.ResolveInfoFlags.of(flags.toLong())
|
||||
)
|
||||
else -> @Suppress("DEPRECATION") queryIntentActivities(data, flags)
|
||||
}
|
||||
}
|
||||
|
||||
fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo {
|
||||
return when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getApplicationInfo(
|
||||
|
|
|
|||
|
|
@ -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">"Ei õnnestunud leida selle tegevuse jaoks vajalikku välist rakendust."</string>
|
||||
</resources>
|
||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
|
|||
enum class AvatarSize(val dp: Dp) {
|
||||
CurrentUserTopBar(32.dp),
|
||||
|
||||
IncomingCall(140.dp),
|
||||
RoomHeader(96.dp),
|
||||
RoomListItem(52.dp),
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
|||
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
|
|
@ -111,6 +112,8 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
|||
prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisambiguatedDisplayName, isDmRoom)
|
||||
}
|
||||
is LegacyCallInviteContent -> sp.getString(CommonStrings.common_call_invite)
|
||||
is CallNotifyContent -> sp.getString(CommonStrings.common_call_started)
|
||||
else -> null
|
||||
}?.take(MAX_SAFE_LENGTH)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ 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.timeline.item.event.CallNotifyContent
|
||||
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
|
||||
|
|
@ -63,6 +64,9 @@ class DefaultTimelineEventFormatter @Inject constructor(
|
|||
is LegacyCallInviteContent -> {
|
||||
sp.getString(CommonStrings.common_call_invite)
|
||||
}
|
||||
is CallNotifyContent -> {
|
||||
sp.getString(CommonStrings.common_call_started)
|
||||
}
|
||||
RedactedContent,
|
||||
is StickerContent,
|
||||
is PollContent,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
<?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">"(tunnuspilt muutus ka)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s muutis oma tunnuspilti"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Sina muutsid oma tunnuspilti"</string>
|
||||
<string name="state_event_demoted_to_member">"%1$s on nüüd tavakasutaja rollis"</string>
|
||||
<string name="state_event_demoted_to_moderator">"%1$s on nüüd moderaatori rollis"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s muutis senise kuvatava nime „%2$s“ asemele uueks nimeks „%3$s“"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Sina muutsid senise kuvatava nime „%1$s“ asemel uueks nimeks „%2$s“"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s eemaldas oma kuvatava nime (mis oli „%2$s“)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Sina eemaldasid oma kuvatava nime (mis oli „%1$s“)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s määras oma kuvatavaks nimeks „%2$s“"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Sina määrasid oma kuvatavaks nimeks „%1$s“"</string>
|
||||
<string name="state_event_promoted_to_administrator">"%1$s on nüüd peakasutaja rollis"</string>
|
||||
<string name="state_event_promoted_to_moderator">"%1$s on nüüd moderaatori rollis"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s muutis jututoa tunnuspilti"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Sina muutsid jututoa tunnuspilti"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s eemaldas jututoa tunnuspildi"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"Sina eemaldasid jututoa tunnuspildi"</string>
|
||||
<string name="state_event_room_ban">"%1$s keelas %2$s ligipääsu"</string>
|
||||
<string name="state_event_room_ban_by_you">"Sina keelasid %1$s ligipääsu"</string>
|
||||
<string name="state_event_room_created">"%1$s lõi jututoa"</string>
|
||||
<string name="state_event_room_created_by_you">"Sina lõid jututoa"</string>
|
||||
<string name="state_event_room_invite">"%1$s saatis kutse kasutajale %2$s"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s võttis kutse vastu"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Sina võtsid kutse vastu"</string>
|
||||
<string name="state_event_room_invite_by_you">"Sina saatsid kutse kasutajale %1$s"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s saatis sulle kutse"</string>
|
||||
<string name="state_event_room_join">"%1$s liitus jututoaga"</string>
|
||||
<string name="state_event_room_join_by_you">"Sina liitusid jututoaga"</string>
|
||||
<string name="state_event_room_knock">"%1$s palus liitumist"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s lubas kasutajal %2$s liituda"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"Sina lubasid kasutajal %1$s liituda"</string>
|
||||
<string name="state_event_room_knock_by_you">"Sina palusid liitumist"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s lükkas tagasi kasutaja %2$s liitumispalve"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"Sina lükkasid tagasi kasutaja %1$s liitumispalve"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s lükkas tagasi sinu liitumispalve"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s pole enam liitumisest huvitatud"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"Sina tühitasid oma liitumissoovi"</string>
|
||||
<string name="state_event_room_leave">"%1$s lahkus jututoast"</string>
|
||||
<string name="state_event_room_leave_by_you">"Sina lahkusid jututoast"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s muutis jututoa uueks nimeks „%2$s“"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Sina muutsid jututoa uueks nimeks „%1$s“"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s eemaldas jututoa nime"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Sina eemaldasid jututoa nime"</string>
|
||||
<string name="state_event_room_none">"%1$s ei teinud ühtegi muudatust"</string>
|
||||
<string name="state_event_room_none_by_you">"Sina ei teinud ühtegi muudatust"</string>
|
||||
<string name="state_event_room_reject">"%1$s lükkas kutse tagasi"</string>
|
||||
<string name="state_event_room_reject_by_you">"Sina lükkasid kutse tagasi"</string>
|
||||
<string name="state_event_room_remove">"%1$s eemaldas jututoast kasutaja %2$s"</string>
|
||||
<string name="state_event_room_remove_by_you">"Sina eemaldasid jututoast kasutaja %1$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s saatis jututoaga liitumiseks kutse kasutajale %2$s"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Sina saatsid kasutajale %1$s kutse jututoaga liitumiseks"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s võttis tagasi jututoaga liitumise kutse kasutajalt %2$s"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Sina võtsid tagasi jututoaga liitumise kutse kasutajalt %1$s"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s muutis uueks teemaks %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Sina muutsid uueks teemaks %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s eemaldas jututoa teema"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Sina eemaldasid jututoa teema"</string>
|
||||
<string name="state_event_room_unban">"%1$s taastas %2$s ligipääsu"</string>
|
||||
<string name="state_event_room_unban_by_you">"Sina taastasid %1$s ligipääsu"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s tegi oma liikmelisuses teadmata muutatuse"</string>
|
||||
</resources>
|
||||
|
|
@ -50,6 +50,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.media.aMediaSource
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
import io.element.android.libraries.matrix.test.timeline.aPollContent
|
||||
import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent
|
||||
|
|
@ -106,7 +107,7 @@ class DefaultRoomLastMessageFormatterTest {
|
|||
fun `Sticker content`() {
|
||||
val body = "body"
|
||||
val info = ImageInfo(null, null, null, null, null, null, null)
|
||||
val message = createRoomEvent(false, null, StickerContent(body, info, "url"))
|
||||
val message = createRoomEvent(false, null, StickerContent(body, info, aMediaSource(url = "url")))
|
||||
val result = formatter.format(message, false)
|
||||
assertThat(result).isEqualTo(body)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,4 +96,11 @@ enum class FeatureFlags(
|
|||
defaultValue = true,
|
||||
isFinished = false,
|
||||
),
|
||||
IncomingShare(
|
||||
key = "feature.incomingShare",
|
||||
title = "Incoming Share support",
|
||||
description = "Allow the application to receive data from other applications",
|
||||
defaultValue = true,
|
||||
isFinished = false,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
|
|||
FeatureFlags.RoomDirectorySearch -> false
|
||||
FeatureFlags.ShowBlockedUsersDetails -> false
|
||||
FeatureFlags.QrCodeLogin -> OnBoardingConfig.CAN_LOGIN_WITH_QR_CODE
|
||||
FeatureFlags.IncomingShare -> true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
|
|
|||
|
|
@ -33,3 +33,5 @@ value class ThreadId(val value: String) : Serializable {
|
|||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
fun ThreadId.asEventId(): EventId = EventId(value)
|
||||
|
|
|
|||
|
|
@ -330,5 +330,10 @@ interface MatrixRoom : Closeable {
|
|||
*/
|
||||
suspend fun getPermalinkFor(eventId: EventId): Result<String>
|
||||
|
||||
/**
|
||||
* Send an Element Call started notification if needed.
|
||||
*/
|
||||
suspend fun sendCallNotificationIfNeeded(): Result<Unit>
|
||||
|
||||
override fun close() = destroy()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,13 @@ interface Timeline : AutoCloseable {
|
|||
|
||||
suspend fun enterSpecialMode(eventId: EventId?): Result<Unit>
|
||||
|
||||
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
|
||||
suspend fun replyMessage(
|
||||
eventId: EventId,
|
||||
body: String,
|
||||
htmlBody: String?,
|
||||
mentions: List<Mention>,
|
||||
fromNotification: Boolean = false,
|
||||
): Result<Unit>
|
||||
|
||||
suspend fun sendImage(
|
||||
file: File,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.api.timeline.item.event
|
|||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -40,7 +41,7 @@ data object RedactedContent : EventContent
|
|||
data class StickerContent(
|
||||
val body: String,
|
||||
val info: ImageInfo,
|
||||
val url: String
|
||||
val source: MediaSource,
|
||||
) : EventContent
|
||||
|
||||
data class PollContent(
|
||||
|
|
@ -102,4 +103,6 @@ data class FailedToParseStateContent(
|
|||
|
||||
data object LegacyCallInviteContent : EventContent
|
||||
|
||||
data object CallNotifyContent : EventContent
|
||||
|
||||
data object UnknownContent : EventContent
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ object EventType {
|
|||
const val CALL_NEGOTIATE = "m.call.negotiate"
|
||||
const val CALL_REJECT = "m.call.reject"
|
||||
const val CALL_HANGUP = "m.call.hangup"
|
||||
const val CALL_NOTIFY = "m.call.notify"
|
||||
|
||||
// This type is not processed by the client, just sent to the server
|
||||
const val CALL_REPLACES = "m.call.replaces"
|
||||
|
|
@ -94,6 +95,7 @@ object EventType {
|
|||
type == CALL_SELECT_ANSWER ||
|
||||
type == CALL_NEGOTIATE ||
|
||||
type == CALL_REJECT ||
|
||||
type == CALL_REPLACES
|
||||
type == CALL_REPLACES ||
|
||||
type == CALL_NOTIFY
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,8 @@
|
|||
package io.element.android.libraries.matrix.api.notification
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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_USER_ID
|
||||
import io.element.android.libraries.matrix.test.notification.aNotificationData
|
||||
import org.junit.Test
|
||||
|
||||
class NotificationDataTest {
|
||||
|
|
@ -49,25 +48,4 @@ class NotificationDataTest {
|
|||
)
|
||||
assertThat(sut.getDisambiguatedDisplayName(A_USER_ID)).isEqualTo("Alice (@alice:server.org)")
|
||||
}
|
||||
|
||||
private fun aNotificationData(
|
||||
senderDisplayName: String?,
|
||||
senderIsNameAmbiguous: Boolean,
|
||||
): NotificationData {
|
||||
return NotificationData(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
senderAvatarUrl = null,
|
||||
senderDisplayName = senderDisplayName,
|
||||
senderIsNameAmbiguous = senderIsNameAmbiguous,
|
||||
roomAvatarUrl = null,
|
||||
roomDisplayName = null,
|
||||
isDirect = false,
|
||||
isEncrypted = false,
|
||||
isNoisy = false,
|
||||
timestamp = 0L,
|
||||
content = NotificationContent.MessageLike.RoomEncrypted,
|
||||
hasMention = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import org.matrix.rustcomponents.sdk.RoomListService
|
|||
import org.matrix.rustcomponents.sdk.RoomSubscription
|
||||
import timber.log.Timber
|
||||
|
||||
private const val DEFAULT_TIMELINE_LIMIT = 20u
|
||||
|
||||
class RoomSyncSubscriber(
|
||||
private val roomListService: RoomListService,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
|
|
@ -41,8 +43,9 @@ class RoomSyncSubscriber(
|
|||
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
|
||||
),
|
||||
timelineLimit = null,
|
||||
includeHeroes = true,
|
||||
timelineLimit = DEFAULT_TIMELINE_LIMIT,
|
||||
// We don't need heroes here as they're already included in the `all_rooms` list
|
||||
includeHeroes = false,
|
||||
)
|
||||
|
||||
suspend fun subscribe(roomId: RoomId) = mutex.withLock {
|
||||
|
|
|
|||
|
|
@ -590,6 +590,10 @@ class RustMatrixRoom(
|
|||
innerRoom.matrixToEventPermalink(eventId.value)
|
||||
}
|
||||
|
||||
override suspend fun sendCallNotificationIfNeeded(): Result<Unit> = runCatching {
|
||||
innerRoom.sendCallNotificationIfNeeded()
|
||||
}
|
||||
|
||||
private fun createTimeline(
|
||||
timeline: InnerTimeline,
|
||||
isLive: Boolean,
|
||||
|
|
|
|||
|
|
@ -25,11 +25,27 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import org.matrix.rustcomponents.sdk.PaginationStatusListener
|
||||
import org.matrix.rustcomponents.sdk.Timeline
|
||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||
import org.matrix.rustcomponents.sdk.TimelineItem
|
||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk_ui.LiveBackPaginationStatus
|
||||
|
||||
internal fun Timeline.liveBackPaginationStatus(): Flow<LiveBackPaginationStatus> = callbackFlow {
|
||||
val listener = object : PaginationStatusListener {
|
||||
override fun onUpdate(status: LiveBackPaginationStatus) {
|
||||
trySend(status)
|
||||
}
|
||||
}
|
||||
val result = subscribeToBackPaginationStatus(listener)
|
||||
awaitClose {
|
||||
result.cancelAndDestroy()
|
||||
}
|
||||
}.catch {
|
||||
Timber.d(it, "liveBackPaginationStatus() failed")
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List<TimelineItem>) -> Unit): Flow<List<TimelineDiff>> =
|
||||
callbackFlow {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
|||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk_ui.EventItemOrigin
|
||||
import uniffi.matrix_sdk_ui.LiveBackPaginationStatus
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
|
@ -154,6 +155,21 @@ class RustTimeline(
|
|||
launch {
|
||||
fetchMembers()
|
||||
}
|
||||
|
||||
if (isLive) {
|
||||
// When timeline is live, we need to listen to the back pagination status as
|
||||
// sdk can automatically paginate backwards.
|
||||
inner.liveBackPaginationStatus()
|
||||
.onEach { backPaginationStatus ->
|
||||
updatePaginationStatus(Timeline.PaginationDirection.BACKWARDS) {
|
||||
when (backPaginationStatus) {
|
||||
is LiveBackPaginationStatus.Idle -> it.copy(isPaginating = false, hasMoreToLoad = !backPaginationStatus.hitStartOfTimeline)
|
||||
is LiveBackPaginationStatus.Paginating -> it.copy(isPaginating = true, hasMoreToLoad = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -333,13 +349,28 @@ class RustTimeline(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = withContext(dispatcher) {
|
||||
override suspend fun replyMessage(
|
||||
eventId: EventId,
|
||||
body: String,
|
||||
htmlBody: String?,
|
||||
mentions: List<Mention>,
|
||||
fromNotification: Boolean,
|
||||
): Result<Unit> = withContext(dispatcher) {
|
||||
runCatching {
|
||||
val inReplyTo = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(eventId.value)
|
||||
inReplyTo.use { eventTimelineItem ->
|
||||
inner.sendReply(messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), eventTimelineItem)
|
||||
val msg = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map())
|
||||
if (fromNotification) {
|
||||
// When replying from a notification, do not interfere with `specialModeEventTimelineItem`
|
||||
val inReplyTo = inner.getEventTimelineItemByEventId(eventId.value)
|
||||
inReplyTo.use { eventTimelineItem ->
|
||||
inner.sendReply(msg, eventTimelineItem)
|
||||
}
|
||||
} else {
|
||||
val inReplyTo = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(eventId.value)
|
||||
inReplyTo.use { eventTimelineItem ->
|
||||
inner.sendReply(msg, eventTimelineItem)
|
||||
}
|
||||
specialModeEventTimelineItem = null
|
||||
}
|
||||
specialModeEventTimelineItem = null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.matrix.impl.timeline.item.event
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
|
|
@ -103,7 +104,7 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
|
|||
StickerContent(
|
||||
body = kind.body,
|
||||
info = kind.info.map(),
|
||||
url = kind.url,
|
||||
source = kind.source.map(),
|
||||
)
|
||||
}
|
||||
is TimelineItemContentKind.Poll -> {
|
||||
|
|
@ -125,6 +126,7 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
|
|||
)
|
||||
}
|
||||
is TimelineItemContentKind.CallInvite -> LegacyCallInviteContent
|
||||
is TimelineItemContentKind.CallNotify -> CallNotifyContent
|
||||
else -> UnknownContent
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.test.notification
|
||||
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
|
||||
fun aNotificationData(
|
||||
senderDisplayName: String?,
|
||||
senderIsNameAmbiguous: Boolean,
|
||||
): NotificationData {
|
||||
return NotificationData(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
senderAvatarUrl = null,
|
||||
senderDisplayName = senderDisplayName,
|
||||
senderIsNameAmbiguous = senderIsNameAmbiguous,
|
||||
roomAvatarUrl = null,
|
||||
roomDisplayName = null,
|
||||
isDirect = false,
|
||||
isEncrypted = false,
|
||||
isNoisy = false,
|
||||
timestamp = 0L,
|
||||
content = NotificationContent.MessageLike.RoomEncrypted,
|
||||
hasMention = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -86,6 +86,7 @@ class FakeMatrixRoom(
|
|||
override val liveTimeline: Timeline = FakeTimeline(),
|
||||
private var roomPermalinkResult: () -> Result<String> = { Result.success("room link") },
|
||||
private var eventPermalinkResult: (EventId) -> Result<String> = { Result.success("event link") },
|
||||
var sendCallNotificationIfNeededResult: () -> Result<Unit> = { Result.success(Unit) },
|
||||
canRedactOwn: Boolean = false,
|
||||
canRedactOther: Boolean = false,
|
||||
) : MatrixRoom {
|
||||
|
|
@ -520,6 +521,10 @@ class FakeMatrixRoom(
|
|||
theme: String?,
|
||||
): Result<String> = generateWidgetWebViewUrlResult
|
||||
|
||||
override suspend fun sendCallNotificationIfNeeded(): Result<Unit> {
|
||||
return sendCallNotificationIfNeededResult()
|
||||
}
|
||||
|
||||
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = getWidgetDriverResult
|
||||
|
||||
fun givenRoomMembersState(state: MatrixRoomMembersState) {
|
||||
|
|
|
|||
|
|
@ -114,7 +114,8 @@ class FakeTimeline(
|
|||
body: String,
|
||||
htmlBody: String?,
|
||||
mentions: List<Mention>,
|
||||
) -> Result<Unit> = { _, _, _, _ ->
|
||||
fromNotification: Boolean,
|
||||
) -> Result<Unit> = { _, _, _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
|
||||
|
|
@ -123,11 +124,13 @@ class FakeTimeline(
|
|||
body: String,
|
||||
htmlBody: String?,
|
||||
mentions: List<Mention>,
|
||||
fromNotification: Boolean,
|
||||
): Result<Unit> = replyMessageLambda(
|
||||
eventId,
|
||||
body,
|
||||
htmlBody,
|
||||
mentions
|
||||
mentions,
|
||||
fromNotification,
|
||||
)
|
||||
|
||||
var sendImageLambda: (
|
||||
|
|
|
|||
|
|
@ -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">"Selleks, et rakendus saaks kaamerat kasutada, palun luba see süsteemi seadistuses."</string>
|
||||
<string name="dialog_permission_generic">"Palun luba süsteemi seadistustest vajalikud õigused."</string>
|
||||
<string name="dialog_permission_microphone">"Selleks, et rakendus saaks mikrofoni kasutada, palun luba see süsteemi seadistuses."</string>
|
||||
<string name="dialog_permission_notification">"Selleks, et rakendus saaks kuvada teavitusi, palun anna vajalikud õiguse süsteemi seadistustes."</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="troubleshoot_notifications_test_check_permission_title">"Täpsusta õigusi"</string>
|
||||
</resources>
|
||||
|
|
@ -21,12 +21,13 @@ import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
|
||||
import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class FakeSessionPreferencesStoreFactory(
|
||||
var getLambda: LambdaTwoParamsRecorder<SessionId, CoroutineScope, SessionPreferencesStore> = lambdaRecorder { _, _ -> throw NotImplementedError() },
|
||||
var removeLambda: LambdaOneParamRecorder<SessionId, Unit> = lambdaRecorder { _ -> },
|
||||
val getLambda: LambdaTwoParamsRecorder<SessionId, CoroutineScope, SessionPreferencesStore> = lambdaRecorder { _, _ -> lambdaError() },
|
||||
val removeLambda: LambdaOneParamRecorder<SessionId, Unit> = lambdaRecorder { _ -> lambdaError() },
|
||||
) : SessionPreferencesStoreFactory {
|
||||
override fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): SessionPreferencesStore {
|
||||
return getLambda(sessionId, sessionCoroutineScope)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ android {
|
|||
dependencies {
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.api.notifications
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import coil.ImageLoader
|
||||
|
||||
interface NotificationBitmapLoader {
|
||||
/**
|
||||
* Get icon of a room.
|
||||
* @param path mxc url
|
||||
* @param imageLoader Coil image loader
|
||||
*/
|
||||
suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap?
|
||||
|
||||
/**
|
||||
* 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?, imageLoader: ImageLoader): IconCompat?
|
||||
}
|
||||
|
|
@ -16,10 +16,15 @@
|
|||
|
||||
package io.element.android.libraries.push.api.notifications
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface NotificationDrawerManager {
|
||||
fun clearAllMessagesEvents(sessionId: SessionId)
|
||||
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId)
|
||||
fun clearEvent(sessionId: SessionId, eventId: EventId)
|
||||
|
||||
fun clearMembershipNotificationForSession(sessionId: SessionId)
|
||||
fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
* Copyright (c) 2024 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.
|
||||
|
|
@ -14,13 +14,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
package io.element.android.libraries.push.api.notifications
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
|
||||
class NotificationIdProvider @Inject constructor() {
|
||||
object NotificationIdProvider {
|
||||
fun getSummaryNotificationId(sessionId: SessionId): Int {
|
||||
return getOffset(sessionId) + SUMMARY_NOTIFICATION_ID
|
||||
}
|
||||
|
|
@ -41,16 +40,30 @@ class NotificationIdProvider @Inject constructor() {
|
|||
return getOffset(sessionId) + FALLBACK_NOTIFICATION_ID
|
||||
}
|
||||
|
||||
fun getCallNotificationId(sessionId: SessionId): Int {
|
||||
return getOffset(sessionId) + ROOM_CALL_NOTIFICATION_ID
|
||||
}
|
||||
|
||||
fun getForegroundServiceNotificationId(type: ForegroundServiceType): Int {
|
||||
return type.id * 10 + FOREGROUND_SERVICE_NOTIFICATION_ID
|
||||
}
|
||||
|
||||
private fun getOffset(sessionId: SessionId): Int {
|
||||
// Compute a int from a string with a low risk of collision.
|
||||
return abs(sessionId.value.hashCode() % 100_000) * 10
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FALLBACK_NOTIFICATION_ID = -1
|
||||
private const val SUMMARY_NOTIFICATION_ID = 0
|
||||
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
|
||||
private const val ROOM_EVENT_NOTIFICATION_ID = 2
|
||||
private const val ROOM_INVITATION_NOTIFICATION_ID = 3
|
||||
}
|
||||
private const val FALLBACK_NOTIFICATION_ID = -1
|
||||
private const val SUMMARY_NOTIFICATION_ID = 0
|
||||
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
|
||||
private const val ROOM_EVENT_NOTIFICATION_ID = 2
|
||||
private const val ROOM_INVITATION_NOTIFICATION_ID = 3
|
||||
private const val ROOM_CALL_NOTIFICATION_ID = 3
|
||||
|
||||
private const val FOREGROUND_SERVICE_NOTIFICATION_ID = 4
|
||||
}
|
||||
|
||||
enum class ForegroundServiceType(val id: Int) {
|
||||
INCOMING_CALL(1),
|
||||
ONGOING_CALL(2),
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.api.notifications
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
/**
|
||||
* Handles missed calls by creating a new notification.
|
||||
*/
|
||||
interface OnMissedCallNotificationHandler {
|
||||
/**
|
||||
* Adds a missed call notification.
|
||||
*/
|
||||
suspend fun addMissedCallNotification(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
eventId: EventId,
|
||||
)
|
||||
}
|
||||
|
|
@ -55,6 +55,7 @@ dependencies {
|
|||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.troubleshoot.api)
|
||||
implementation(projects.features.call.api)
|
||||
api(projects.libraries.pushproviders.api)
|
||||
api(projects.libraries.pushstore.api)
|
||||
api(projects.libraries.push.api)
|
||||
|
|
@ -71,10 +72,12 @@ dependencies {
|
|||
testImplementation(libs.coil.test)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
testImplementation(projects.libraries.pushstore.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.features.call.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
testImplementation(projects.services.toolbox.impl)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.squareup.anvil.annotations.ContributesBinding
|
|||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
interface ActiveNotificationsProvider {
|
||||
|
|
@ -37,7 +38,6 @@ interface ActiveNotificationsProvider {
|
|||
@ContributesBinding(AppScope::class)
|
||||
class DefaultActiveNotificationsProvider @Inject constructor(
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
private val notificationIdProvider: NotificationIdProvider,
|
||||
) : ActiveNotificationsProvider {
|
||||
override fun getAllNotifications(): List<StatusBarNotification> {
|
||||
return notificationManager.activeNotifications
|
||||
|
|
@ -48,22 +48,22 @@ class DefaultActiveNotificationsProvider @Inject constructor(
|
|||
}
|
||||
|
||||
override fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification> {
|
||||
val notificationId = notificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
||||
val notificationId = NotificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
||||
return getNotificationsForSession(sessionId).filter { it.id == notificationId }
|
||||
}
|
||||
|
||||
override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
||||
val notificationId = notificationIdProvider.getRoomMessagesNotificationId(sessionId)
|
||||
val notificationId = NotificationIdProvider.getRoomMessagesNotificationId(sessionId)
|
||||
return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value }
|
||||
}
|
||||
|
||||
override fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
||||
val notificationId = notificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
||||
val notificationId = NotificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
||||
return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value }
|
||||
}
|
||||
|
||||
override fun getSummaryNotification(sessionId: SessionId): StatusBarNotification? {
|
||||
val summaryId = notificationIdProvider.getSummaryNotificationId(sessionId)
|
||||
val summaryId = NotificationIdProvider.getSummaryNotificationId(sessionId)
|
||||
return getNotificationsForSession(sessionId).find { it.id == summaryId }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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 io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Helper to resolve a valid [NotifiableEvent] from a [NotificationData].
|
||||
*/
|
||||
interface CallNotificationEventResolver {
|
||||
/**
|
||||
* Resolve a call notification event from a notification data depending on whether it should be a ringing one or not.
|
||||
* @param sessionId the current session id
|
||||
* @param notificationData the notification data
|
||||
* @param forceNotify `true` to force the notification to be non-ringing, `false` to use the default behaviour. Default is `false`.
|
||||
* @return a [NotifiableEvent] if the notification data is a call notification, null otherwise
|
||||
*/
|
||||
fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean = false): NotifiableEvent?
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultCallNotificationEventResolver @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
) : CallNotificationEventResolver {
|
||||
override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): NotifiableEvent? {
|
||||
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify ?: return null
|
||||
return notificationData.run {
|
||||
if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp) && !forceNotify) {
|
||||
NotifiableRingingCallEvent(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
roomName = roomDisplayName,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
timestamp = this.timestamp,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
description = stringProvider.getString(R.string.notification_incoming_call),
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
roomAvatarUrl = roomAvatarUrl,
|
||||
callNotifyType = content.type,
|
||||
senderId = content.senderId,
|
||||
senderAvatarUrl = senderAvatarUrl,
|
||||
)
|
||||
} else {
|
||||
// Create a simple message notification event
|
||||
buildNotifiableMessageEvent(
|
||||
sessionId = sessionId,
|
||||
senderId = content.senderId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
noisy = true,
|
||||
timestamp = this.timestamp,
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
body = "☎️ ${stringProvider.getString(R.string.notification_incoming_call)}",
|
||||
roomName = roomDisplayName,
|
||||
roomIsDirect = isDirect,
|
||||
roomAvatarPath = roomAvatarUrl,
|
||||
senderAvatarPath = senderAvatarUrl,
|
||||
type = EventType.CALL_NOTIFY,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
|||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
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.EventType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
|
||||
|
|
@ -77,6 +78,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
private val notificationMediaRepoFactory: NotificationMediaRepo.Factory,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val callNotificationEventResolver: CallNotificationEventResolver,
|
||||
) : NotifiableEventResolver {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
|
||||
// Restore session
|
||||
|
|
@ -150,8 +152,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
}
|
||||
NotificationContent.MessageLike.CallAnswer,
|
||||
NotificationContent.MessageLike.CallCandidates,
|
||||
NotificationContent.MessageLike.CallHangup,
|
||||
is NotificationContent.MessageLike.CallNotify -> { // TODO CallNotify will be handled separately in the future
|
||||
NotificationContent.MessageLike.CallHangup -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}")
|
||||
null
|
||||
}
|
||||
|
|
@ -172,6 +173,9 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
senderAvatarPath = senderAvatarUrl,
|
||||
)
|
||||
}
|
||||
is NotificationContent.MessageLike.CallNotify -> {
|
||||
callNotificationEventResolver.resolveEvent(userId, this)
|
||||
}
|
||||
NotificationContent.MessageLike.KeyVerificationAccept,
|
||||
NotificationContent.MessageLike.KeyVerificationCancel,
|
||||
NotificationContent.MessageLike.KeyVerificationDone,
|
||||
|
|
@ -310,7 +314,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun buildNotifiableMessageEvent(
|
||||
internal fun buildNotifiableMessageEvent(
|
||||
sessionId: SessionId,
|
||||
senderId: UserId,
|
||||
roomId: RoomId,
|
||||
|
|
@ -334,7 +338,8 @@ private fun buildNotifiableMessageEvent(
|
|||
outGoingMessage: Boolean = false,
|
||||
outGoingMessageFailed: Boolean = false,
|
||||
isRedacted: Boolean = false,
|
||||
isUpdated: Boolean = false
|
||||
isUpdated: Boolean = false,
|
||||
type: String = EventType.MESSAGE,
|
||||
) = NotifiableMessageEvent(
|
||||
sessionId = sessionId,
|
||||
senderId = senderId,
|
||||
|
|
@ -356,5 +361,6 @@ private fun buildNotifiableMessageEvent(
|
|||
outGoingMessage = outGoingMessage,
|
||||
outGoingMessageFailed = outGoingMessageFailed,
|
||||
isRedacted = isRedacted,
|
||||
isUpdated = isUpdated
|
||||
isUpdated = isUpdated,
|
||||
type = type,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,23 +24,27 @@ import androidx.core.graphics.drawable.toBitmap
|
|||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
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.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationBitmapLoader @Inject constructor(
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultNotificationBitmapLoader @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val sdkIntProvider: BuildVersionSdkIntProvider,
|
||||
) {
|
||||
) : NotificationBitmapLoader {
|
||||
/**
|
||||
* Get icon of a room.
|
||||
* @param path mxc url
|
||||
* @param imageLoader Coil image loader
|
||||
*/
|
||||
suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? {
|
||||
override suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? {
|
||||
if (path == null) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -67,7 +71,7 @@ class NotificationBitmapLoader @Inject constructor(
|
|||
* @param path mxc url
|
||||
* @param imageLoader Coil image loader
|
||||
*/
|
||||
suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? {
|
||||
override suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? {
|
||||
if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ 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.api.notifications.NotificationIdProvider
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
|
|
@ -53,7 +54,6 @@ private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.
|
|||
class DefaultNotificationDrawerManager @Inject constructor(
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
private val notificationRenderer: NotificationRenderer,
|
||||
private val notificationIdProvider: NotificationIdProvider,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
coroutineScope: CoroutineScope,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
|
|
@ -123,8 +123,8 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
/**
|
||||
* Clear all known message events for a [sessionId].
|
||||
*/
|
||||
fun clearAllMessagesEvents(sessionId: SessionId) {
|
||||
notificationManager.cancel(null, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||
override fun clearAllMessagesEvents(sessionId: SessionId) {
|
||||
notificationManager.cancel(null, NotificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||
clearSummaryNotificationIfNeeded(sessionId)
|
||||
}
|
||||
|
||||
|
|
@ -141,8 +141,8 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
|
||||
* Can also be called when a notification for this room is dismissed by the user.
|
||||
*/
|
||||
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
notificationManager.cancel(roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||
override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
notificationManager.cancel(roomId.value, NotificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||
clearSummaryNotificationIfNeeded(sessionId)
|
||||
}
|
||||
|
||||
|
|
@ -164,8 +164,8 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
/**
|
||||
* Clear the notifications for a single event.
|
||||
*/
|
||||
fun clearEvent(sessionId: SessionId, eventId: EventId) {
|
||||
val id = notificationIdProvider.getRoomEventNotificationId(sessionId)
|
||||
override fun clearEvent(sessionId: SessionId, eventId: EventId) {
|
||||
val id = NotificationIdProvider.getRoomEventNotificationId(sessionId)
|
||||
notificationManager.cancel(eventId.value, id)
|
||||
clearSummaryNotificationIfNeeded(sessionId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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 io.element.android.libraries.di.AppScope
|
||||
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
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultOnMissedCallNotificationHandler @Inject constructor(
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager,
|
||||
private val callNotificationEventResolver: CallNotificationEventResolver,
|
||||
) : OnMissedCallNotificationHandler {
|
||||
override suspend fun addMissedCallNotification(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
eventId: EventId,
|
||||
) {
|
||||
// Resolve the event and add a notification for it, at this point it should no longer be a ringing one
|
||||
val notificationData = matrixClientProvider.getOrRestore(sessionId).getOrNull()
|
||||
?.notificationService()
|
||||
?.getNotification(sessionId, roomId, eventId)
|
||||
?.getOrNull()
|
||||
?: return
|
||||
|
||||
val notifiableEvent = callNotificationEventResolver.resolveEvent(
|
||||
sessionId = sessionId,
|
||||
notificationData = notificationData,
|
||||
// Make sure the notifiable event is not a ringing one
|
||||
forceNotify = true,
|
||||
)
|
||||
notifiableEvent?.let { defaultNotificationDrawerManager.onNotifiableEventReceived(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -20,16 +20,13 @@ import io.element.android.libraries.core.meta.BuildMeta
|
|||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Util class for creating notifications.
|
||||
* Note: Cannot inject ColorProvider in the constructor, because it requires an Activity
|
||||
* Util class for creating notifications action Ids, using the application id.
|
||||
*/
|
||||
|
||||
data class NotificationActionIds @Inject constructor(
|
||||
private val buildMeta: BuildMeta,
|
||||
) {
|
||||
val join = "${buildMeta.applicationId}.NotificationActions.JOIN_ACTION"
|
||||
val reject = "${buildMeta.applicationId}.NotificationActions.REJECT_ACTION"
|
||||
val quickLaunch = "${buildMeta.applicationId}.NotificationActions.QUICK_LAUNCH_ACTION"
|
||||
val markRoomRead = "${buildMeta.applicationId}.NotificationActions.MARK_ROOM_READ_ACTION"
|
||||
val smartReply = "${buildMeta.applicationId}.NotificationActions.SMART_REPLY_ACTION"
|
||||
val dismissSummary = "${buildMeta.applicationId}.NotificationActions.DISMISS_SUMMARY_ACTION"
|
||||
|
|
@ -37,5 +34,4 @@ data class NotificationActionIds @Inject constructor(
|
|||
val dismissInvite = "${buildMeta.applicationId}.NotificationActions.DISMISS_INVITE_NOTIF_ACTION"
|
||||
val dismissEvent = "${buildMeta.applicationId}.NotificationActions.DISMISS_EVENT_NOTIF_ACTION"
|
||||
val diagnostic = "${buildMeta.applicationId}.NotificationActions.DIAGNOSTIC"
|
||||
val push = "${buildMeta.applicationId}.PUSH"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,211 +19,21 @@ package io.element.android.libraries.push.impl.notifications
|
|||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
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
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("NotificationBroadcastReceiver", LoggerTag.NotificationLoggerTag)
|
||||
|
||||
/**
|
||||
* Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.).
|
||||
*/
|
||||
class NotificationBroadcastReceiver : BroadcastReceiver() {
|
||||
@Inject lateinit var appCoroutineScope: CoroutineScope
|
||||
@Inject lateinit var matrixClientProvider: MatrixClientProvider
|
||||
@Inject lateinit var sessionPreferencesStore: SessionPreferencesStoreFactory
|
||||
@Inject lateinit var defaultNotificationDrawerManager: DefaultNotificationDrawerManager
|
||||
@Inject lateinit var actionIds: NotificationActionIds
|
||||
@Inject lateinit var notificationBroadcastReceiverHandler: NotificationBroadcastReceiverHandler
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent == null || context == null) return
|
||||
val sessionId = intent.extras?.getString(KEY_SESSION_ID)?.let(::SessionId) ?: return
|
||||
val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId)
|
||||
val eventId = intent.getStringExtra(KEY_EVENT_ID)?.let(::EventId)
|
||||
|
||||
context.bindings<NotificationBroadcastReceiverBindings>().inject(this)
|
||||
|
||||
Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}")
|
||||
when (intent.action) {
|
||||
actionIds.smartReply ->
|
||||
handleSmartReply(intent, context)
|
||||
actionIds.dismissRoom -> if (roomId != null) {
|
||||
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
|
||||
}
|
||||
actionIds.dismissSummary ->
|
||||
defaultNotificationDrawerManager.clearAllMessagesEvents(sessionId)
|
||||
actionIds.dismissInvite -> if (roomId != null) {
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||
}
|
||||
actionIds.dismissEvent -> if (eventId != null) {
|
||||
defaultNotificationDrawerManager.clearEvent(sessionId, eventId)
|
||||
}
|
||||
actionIds.markRoomRead -> if (roomId != null) {
|
||||
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
|
||||
handleMarkAsRead(sessionId, roomId)
|
||||
}
|
||||
actionIds.join -> if (roomId != null) {
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||
handleJoinRoom(sessionId, roomId)
|
||||
}
|
||||
actionIds.reject -> if (roomId != null) {
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||
handleRejectRoom(sessionId, roomId)
|
||||
}
|
||||
}
|
||||
notificationBroadcastReceiverHandler.onReceive(intent)
|
||||
}
|
||||
|
||||
private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||
client.joinRoom(roomId)
|
||||
}
|
||||
|
||||
private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||
client.getRoom(roomId)?.leave()
|
||||
}
|
||||
|
||||
private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||
val isSendPublicReadReceiptsEnabled = sessionPreferencesStore.get(sessionId, this).isSendPublicReadReceiptsEnabled().first()
|
||||
val receiptType = if (isSendPublicReadReceiptsEnabled) {
|
||||
ReceiptType.READ
|
||||
} else {
|
||||
ReceiptType.READ_PRIVATE
|
||||
}
|
||||
client.getRoom(roomId)?.markAsRead(receiptType = receiptType)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun handleSmartReply(intent: Intent, context: Context) {
|
||||
/*
|
||||
val message = getReplyMessage(intent)
|
||||
val sessionId = intent.getStringExtra(KEY_SESSION_ID)?.let(::SessionId)
|
||||
val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId)
|
||||
val threadId = intent.getStringExtra(KEY_THREAD_ID)?.let(::ThreadId)
|
||||
|
||||
if (message.isNullOrBlank() || roomId == null) {
|
||||
// ignore this event
|
||||
// Can this happen? should we update notification?
|
||||
return
|
||||
}
|
||||
activeSessionHolder.getActiveSession().let { session ->
|
||||
session.getRoom(roomId)?.let { room ->
|
||||
sendMatrixEvent(message, threadId, session, room, context)
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/*
|
||||
private fun sendMatrixEvent(message: String, threadId: String?, session: Session, room: Room, context: Context?) {
|
||||
if (threadId != null) {
|
||||
room.relationService().replyInThread(
|
||||
rootThreadEventId = threadId,
|
||||
replyInThreadText = message,
|
||||
)
|
||||
} else {
|
||||
room.sendService().sendTextMessage(message)
|
||||
}
|
||||
|
||||
// Create a new event to be displayed in the notification drawer, right now
|
||||
|
||||
val notifiableMessageEvent = NotifiableMessageEvent(
|
||||
// Generate a Fake event id
|
||||
eventId = UUID.randomUUID().toString(),
|
||||
editedEventId = null,
|
||||
noisy = false,
|
||||
timestamp = clock.epochMillis(),
|
||||
senderName = session.roomService().getRoomMember(session.myUserId, room.roomId)?.displayName
|
||||
?: context?.getString(R.string.notification_sender_me),
|
||||
senderId = session.myUserId,
|
||||
body = message,
|
||||
imageUriString = null,
|
||||
roomId = room.roomId,
|
||||
threadId = threadId,
|
||||
roomName = room.roomSummary()?.displayName ?: room.roomId,
|
||||
roomIsDirect = room.roomSummary()?.isDirect == true,
|
||||
outGoingMessage = true,
|
||||
canBeReplaced = false
|
||||
)
|
||||
|
||||
notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(notifiableMessageEvent) }
|
||||
|
||||
/*
|
||||
// TODO Error cannot be managed the same way than in Riot
|
||||
|
||||
val event = Event(mxMessage, session.credentials.userId, roomId)
|
||||
room.storeOutgoingEvent(event)
|
||||
room.sendEvent(event, object : MatrixCallback<Void?> {
|
||||
override fun onSuccess(info: Void?) {
|
||||
Timber.v("Send message : onSuccess ")
|
||||
}
|
||||
|
||||
override fun onNetworkError(e: Exception) {
|
||||
Timber.e(e, "Send message : onNetworkError")
|
||||
onSmartReplyFailed(e.localizedMessage)
|
||||
}
|
||||
|
||||
override fun onMatrixError(e: MatrixError) {
|
||||
Timber.v("Send message : onMatrixError " + e.message)
|
||||
if (e is MXCryptoError) {
|
||||
Toast.makeText(context, e.detailedErrorDescription, Toast.LENGTH_SHORT).show()
|
||||
onSmartReplyFailed(e.detailedErrorDescription)
|
||||
} else {
|
||||
Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show()
|
||||
onSmartReplyFailed(e.localizedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUnexpectedError(e: Exception) {
|
||||
Timber.e(e, "Send message : onUnexpectedError " + e.message)
|
||||
onSmartReplyFailed(e.message)
|
||||
}
|
||||
|
||||
|
||||
fun onSmartReplyFailed(reason: String?) {
|
||||
val notifiableMessageEvent = NotifiableMessageEvent(
|
||||
event.eventId,
|
||||
false,
|
||||
clock.epochMillis(),
|
||||
session.myUser?.displayname
|
||||
?: context?.getString(R.string.notification_sender_me),
|
||||
session.myUserId,
|
||||
message,
|
||||
roomId,
|
||||
room.getRoomDisplayName(context),
|
||||
room.isDirect)
|
||||
notifiableMessageEvent.outGoingMessage = true
|
||||
notifiableMessageEvent.outGoingMessageFailed = true
|
||||
|
||||
VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
|
||||
VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(null)
|
||||
}
|
||||
})
|
||||
*/
|
||||
}
|
||||
|
||||
private fun getReplyMessage(intent: Intent?): String? {
|
||||
if (intent != null) {
|
||||
val remoteInput = RemoteInput.getResultsFromIntent(intent)
|
||||
if (remoteInput != null) {
|
||||
return remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
*/
|
||||
|
||||
companion object {
|
||||
const val KEY_SESSION_ID = "sessionID"
|
||||
const val KEY_ROOM_ID = "roomID"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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 android.content.Intent
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
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
|
||||
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.core.asEventId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("NotificationBroadcastReceiverHandler", LoggerTag.NotificationLoggerTag)
|
||||
|
||||
class NotificationBroadcastReceiverHandler @Inject constructor(
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
private val sessionPreferencesStore: SessionPreferencesStoreFactory,
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
private val actionIds: NotificationActionIds,
|
||||
private val systemClock: SystemClock,
|
||||
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
||||
private val stringProvider: StringProvider,
|
||||
private val replyMessageExtractor: ReplyMessageExtractor,
|
||||
) {
|
||||
fun onReceive(intent: Intent) {
|
||||
val sessionId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return
|
||||
val roomId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_ROOM_ID)?.let(::RoomId)
|
||||
val threadId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_THREAD_ID)?.let(::ThreadId)
|
||||
val eventId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_EVENT_ID)?.let(::EventId)
|
||||
|
||||
Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}")
|
||||
when (intent.action) {
|
||||
actionIds.smartReply -> if (roomId != null) {
|
||||
handleSmartReply(sessionId, roomId, threadId, intent)
|
||||
}
|
||||
actionIds.dismissRoom -> if (roomId != null) {
|
||||
notificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
|
||||
}
|
||||
actionIds.dismissSummary ->
|
||||
notificationDrawerManager.clearAllMessagesEvents(sessionId)
|
||||
actionIds.dismissInvite -> if (roomId != null) {
|
||||
notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||
}
|
||||
actionIds.dismissEvent -> if (eventId != null) {
|
||||
notificationDrawerManager.clearEvent(sessionId, eventId)
|
||||
}
|
||||
actionIds.markRoomRead -> if (roomId != null) {
|
||||
notificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
|
||||
handleMarkAsRead(sessionId, roomId)
|
||||
}
|
||||
actionIds.join -> if (roomId != null) {
|
||||
notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||
handleJoinRoom(sessionId, roomId)
|
||||
}
|
||||
actionIds.reject -> if (roomId != null) {
|
||||
notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||
handleRejectRoom(sessionId, roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||
client.joinRoom(roomId)
|
||||
}
|
||||
|
||||
private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||
client.getRoom(roomId)?.leave()
|
||||
}
|
||||
|
||||
private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||
val isSendPublicReadReceiptsEnabled = sessionPreferencesStore.get(sessionId, this).isSendPublicReadReceiptsEnabled().first()
|
||||
val receiptType = if (isSendPublicReadReceiptsEnabled) {
|
||||
ReceiptType.READ
|
||||
} else {
|
||||
ReceiptType.READ_PRIVATE
|
||||
}
|
||||
client.getRoom(roomId)?.markAsRead(receiptType = receiptType)
|
||||
}
|
||||
|
||||
private fun handleSmartReply(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
threadId: ThreadId?,
|
||||
intent: Intent,
|
||||
) = appCoroutineScope.launch {
|
||||
val message = replyMessageExtractor.getReplyMessage(intent)
|
||||
if (message.isNullOrBlank()) {
|
||||
// ignore this event
|
||||
// Can this happen? should we update notification?
|
||||
return@launch
|
||||
}
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||
client.getRoom(roomId)?.let { room ->
|
||||
sendMatrixEvent(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
threadId = threadId,
|
||||
room = room,
|
||||
message = message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendMatrixEvent(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
threadId: ThreadId?,
|
||||
room: MatrixRoom,
|
||||
message: String,
|
||||
) {
|
||||
// Create a new event to be displayed in the notification drawer, right now
|
||||
val notifiableMessageEvent = NotifiableMessageEvent(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
// Generate a Fake event id
|
||||
eventId = EventId("\$" + UUID.randomUUID().toString()),
|
||||
editedEventId = null,
|
||||
canBeReplaced = false,
|
||||
senderId = sessionId,
|
||||
noisy = false,
|
||||
timestamp = systemClock.epochMillis(),
|
||||
senderDisambiguatedDisplayName = room.getUpdatedMember(sessionId).getOrNull()
|
||||
?.disambiguatedDisplayName
|
||||
?: stringProvider.getString(R.string.notification_sender_me),
|
||||
body = message,
|
||||
imageUriString = null,
|
||||
threadId = threadId,
|
||||
roomName = room.displayName,
|
||||
roomIsDirect = room.isDirect,
|
||||
outGoingMessage = true,
|
||||
)
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent)
|
||||
|
||||
if (threadId != null) {
|
||||
room.liveTimeline.replyMessage(
|
||||
eventId = threadId.asEventId(),
|
||||
body = message,
|
||||
htmlBody = null,
|
||||
mentions = emptyList(),
|
||||
fromNotification = true,
|
||||
)
|
||||
} else {
|
||||
room.liveTimeline.sendMessage(
|
||||
body = message,
|
||||
htmlBody = null,
|
||||
mentions = emptyList()
|
||||
)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed to send smart reply message")
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(
|
||||
notifiableMessageEvent.copy(
|
||||
outGoingMessageFailed = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,8 @@ interface NotificationDataFactory {
|
|||
@JvmName("toNotificationSimpleEvents")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
fun toNotifications(simpleEvents: List<SimpleNotifiableEvent>): List<OneShotNotification>
|
||||
@JvmName("toNotificationFallbackEvents")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification>
|
||||
|
||||
fun createSummaryNotification(
|
||||
|
|
@ -130,6 +132,8 @@ class DefaultNotificationDataFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@JvmName("toNotificationFallbackEvents")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
override fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification> {
|
||||
return fallback.map { event ->
|
||||
OneShotNotification(
|
||||
|
|
|
|||
|
|
@ -19,10 +19,12 @@ 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.user.MatrixUser
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
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.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -30,7 +32,6 @@ import javax.inject.Inject
|
|||
private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.NotificationLoggerTag)
|
||||
|
||||
class NotificationRenderer @Inject constructor(
|
||||
private val notificationIdProvider: NotificationIdProvider,
|
||||
private val notificationDisplayer: NotificationDisplayer,
|
||||
private val notificationDataFactory: NotificationDataFactory,
|
||||
) {
|
||||
|
|
@ -59,14 +60,14 @@ class NotificationRenderer @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("Removing summary notification")
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = null,
|
||||
id = notificationIdProvider.getSummaryNotificationId(currentUser.userId)
|
||||
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId)
|
||||
)
|
||||
}
|
||||
|
||||
roomNotifications.forEach { notificationData ->
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.roomId.value,
|
||||
id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
|
||||
id = NotificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
|
|
@ -76,7 +77,7 @@ class NotificationRenderer @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.key,
|
||||
id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
|
||||
id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
|
|
@ -87,7 +88,7 @@ class NotificationRenderer @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.key,
|
||||
id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId),
|
||||
id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
|
|
@ -98,7 +99,7 @@ class NotificationRenderer @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("Showing fallback notification")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = "FALLBACK",
|
||||
id = notificationIdProvider.getFallbackNotificationId(currentUser.userId),
|
||||
id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
|
||||
notification = fallbackNotifications.first().notification
|
||||
)
|
||||
}
|
||||
|
|
@ -108,7 +109,7 @@ class NotificationRenderer @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("Updating summary notification")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = null,
|
||||
id = notificationIdProvider.getSummaryNotificationId(currentUser.userId),
|
||||
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId),
|
||||
notification = summaryNotification.notification
|
||||
)
|
||||
}
|
||||
|
|
@ -127,6 +128,8 @@ private fun List<NotifiableEvent>.groupByType(): GroupedNotificationEvents {
|
|||
is NotifiableMessageEvent -> roomEvents.add(event.castedToEventType())
|
||||
is SimpleNotifiableEvent -> simpleEvents.add(event.castedToEventType())
|
||||
is FallbackNotifiableEvent -> fallbackEvents.add(event.castedToEventType())
|
||||
// Nothing should be done for ringing call events as they're not handled here
|
||||
is NotifiableRingingCallEvent -> {}
|
||||
}
|
||||
}
|
||||
return GroupedNotificationEvents(roomEvents, simpleEvents, invitationEvents, fallbackEvents)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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 android.content.Intent
|
||||
import androidx.core.app.RemoteInput
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
interface ReplyMessageExtractor {
|
||||
fun getReplyMessage(intent: Intent): String?
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AndroidReplyMessageExtractor @Inject constructor() : ReplyMessageExtractor {
|
||||
override fun getReplyMessage(intent: Intent): String? {
|
||||
return RemoteInput.getResultsFromIntent(intent)
|
||||
?.getCharSequence(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
|
||||
?.toString()
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import com.squareup.anvil.annotations.ContributesBinding
|
|||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.factories.isSmartReplyError
|
||||
|
|
|
|||
|
|
@ -19,10 +19,15 @@ package io.element.android.libraries.push.impl.notifications.channels
|
|||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.media.RingtoneManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
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
|
||||
|
|
@ -30,15 +35,51 @@ import io.element.android.libraries.push.impl.R
|
|||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
/* ==========================================================================================
|
||||
* IDs for channels
|
||||
* ========================================================================================== */
|
||||
private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID"
|
||||
internal const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2"
|
||||
internal const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID"
|
||||
|
||||
// Legacy channel
|
||||
private const val CALL_NOTIFICATION_CHANNEL_ID_V2 = "CALL_NOTIFICATION_CHANNEL_ID_V2"
|
||||
|
||||
internal const val CALL_NOTIFICATION_CHANNEL_ID_V3 = "CALL_NOTIFICATION_CHANNEL_ID_V3"
|
||||
internal const val RINGING_CALL_NOTIFICATION_CHANNEL_ID = "RINGING_CALL_NOTIFICATION_CHANNEL_ID"
|
||||
|
||||
/**
|
||||
* on devices >= android O, we need to define a channel for each notifications.
|
||||
*/
|
||||
interface NotificationChannels {
|
||||
/**
|
||||
* Get the channel for incoming call.
|
||||
* @param ring true if the device should ring when receiving the call.
|
||||
*/
|
||||
fun getChannelForIncomingCall(ring: Boolean): String
|
||||
|
||||
/**
|
||||
* Get the channel for messages.
|
||||
* @param noisy true if the notification should have sound and vibration.
|
||||
*/
|
||||
fun getChannelIdForMessage(noisy: Boolean): String
|
||||
|
||||
/**
|
||||
* Get the channel for test notifications.
|
||||
*/
|
||||
fun getChannelIdForTest(): String
|
||||
}
|
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
|
||||
private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
class NotificationChannels @Inject constructor(
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultNotificationChannels @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
private val stringProvider: StringProvider,
|
||||
) {
|
||||
) : NotificationChannels {
|
||||
init {
|
||||
createNotificationChannels()
|
||||
}
|
||||
|
|
@ -75,6 +116,9 @@ class NotificationChannels @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// Migration - Create new call channel
|
||||
notificationManager.deleteNotificationChannel(CALL_NOTIFICATION_CHANNEL_ID_V2)
|
||||
|
||||
/**
|
||||
* Default notification importance: shows everywhere, makes noise, but does not visually
|
||||
* intrude.
|
||||
|
|
@ -123,46 +167,52 @@ class NotificationChannels @Inject constructor(
|
|||
}
|
||||
)
|
||||
|
||||
// Register a channel for incoming and in progress call notifications with no ringing
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CALL_NOTIFICATION_CHANNEL_ID,
|
||||
CALL_NOTIFICATION_CHANNEL_ID_V3,
|
||||
stringProvider.getString(R.string.notification_channel_call).ifEmpty { "Call" },
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
.apply {
|
||||
description = stringProvider.getString(R.string.notification_channel_call)
|
||||
setSound(null, null)
|
||||
enableVibration(true)
|
||||
enableLights(true)
|
||||
lightColor = accentColor
|
||||
}
|
||||
)
|
||||
|
||||
// Register a channel for incoming call notifications which will ring the device when received
|
||||
val ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE)
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannelCompat.Builder(
|
||||
RINGING_CALL_NOTIFICATION_CHANNEL_ID,
|
||||
NotificationManagerCompat.IMPORTANCE_MAX,
|
||||
)
|
||||
.setName(stringProvider.getString(R.string.notification_channel_ringing_calls).ifEmpty { "Ringing calls" })
|
||||
.setVibrationEnabled(true)
|
||||
.setSound(
|
||||
ringtoneUri,
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setLegacyStreamType(AudioManager.STREAM_RING)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||
.build()
|
||||
)
|
||||
.setDescription(stringProvider.getString(R.string.notification_channel_ringing_calls))
|
||||
.setLightsEnabled(true)
|
||||
.setLightColor(accentColor)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getChannel(channelId: String): NotificationChannel? {
|
||||
return notificationManager.getNotificationChannel(channelId)
|
||||
override fun getChannelForIncomingCall(ring: Boolean): String {
|
||||
return if (ring) RINGING_CALL_NOTIFICATION_CHANNEL_ID else CALL_NOTIFICATION_CHANNEL_ID_V3
|
||||
}
|
||||
|
||||
fun getChannelForIncomingCall(fromBg: Boolean): NotificationChannel? {
|
||||
val notificationChannel = if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
|
||||
return getChannel(notificationChannel)
|
||||
}
|
||||
|
||||
fun getChannelIdForMessage(noisy: Boolean): String {
|
||||
override fun getChannelIdForMessage(noisy: Boolean): String {
|
||||
return if (noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
|
||||
}
|
||||
|
||||
fun getChannelIdForTest(): String = NOISY_NOTIFICATION_CHANNEL_ID
|
||||
|
||||
companion object {
|
||||
/* ==========================================================================================
|
||||
* IDs for channels
|
||||
* ========================================================================================== */
|
||||
private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID"
|
||||
private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2"
|
||||
private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID"
|
||||
private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2"
|
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
|
||||
private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
}
|
||||
override fun getChannelIdForTest(): String = NOISY_NOTIFICATION_CHANNEL_ID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,9 +35,10 @@ import io.element.android.libraries.di.AppScope
|
|||
import io.element.android.libraries.di.ApplicationContext
|
||||
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.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationBitmapLoader
|
||||
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
|
||||
|
|
@ -129,12 +130,16 @@ class DefaultNotificationCreator @Inject constructor(
|
|||
|
||||
val smallIcon = CommonDrawables.ic_notification_small
|
||||
|
||||
val channelId = notificationChannels.getChannelIdForMessage(roomInfo.shouldBing)
|
||||
val containsMissedCall = events.any { it.type == EventType.CALL_NOTIFY }
|
||||
val channelId = if (containsMissedCall) {
|
||||
notificationChannels.getChannelForIncomingCall(false)
|
||||
} else {
|
||||
notificationChannels.getChannelIdForMessage(noisy = roomInfo.shouldBing)
|
||||
}
|
||||
val builder = if (existingNotification != null) {
|
||||
NotificationCompat.Builder(context, existingNotification)
|
||||
} else {
|
||||
NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(roomInfo.isUpdated)
|
||||
// A category allows groups of notifications to be ranked and filtered – per user or system settings.
|
||||
// For example, alarm notifications should display before promo notifications, or message from known contact
|
||||
// that can be displayed in not disturb mode if white listed (the later will need compat28.x)
|
||||
|
|
@ -210,6 +215,11 @@ class DefaultNotificationCreator @Inject constructor(
|
|||
setLargeIcon(largeIcon)
|
||||
}
|
||||
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
|
||||
|
||||
// If any of the events are of call notify type it means a missed call, set the category to the right value
|
||||
if (events.any { it.type == EventType.CALL_NOTIFY }) {
|
||||
setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
|
||||
}
|
||||
}
|
||||
.setTicker(tickerText)
|
||||
.build()
|
||||
|
|
@ -343,7 +353,6 @@ class DefaultNotificationCreator @Inject constructor(
|
|||
.setWhen(lastMessageTimestamp)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setSmallIcon(smallIcon)
|
||||
// set content text to support devices running API level < 24
|
||||
.setGroup(currentUser.userId.value)
|
||||
// set this notification as the summary for the group
|
||||
.setGroupSummary(true)
|
||||
|
|
|
|||
|
|
@ -51,9 +51,9 @@ data class NotifiableMessageEvent(
|
|||
val outGoingMessage: Boolean = false,
|
||||
val outGoingMessageFailed: Boolean = false,
|
||||
override val isRedacted: Boolean = false,
|
||||
override val isUpdated: Boolean = false
|
||||
) : NotifiableEvent {
|
||||
override val isUpdated: Boolean = false,
|
||||
val type: String = EventType.MESSAGE
|
||||
) : NotifiableEvent {
|
||||
override val description: String = body ?: ""
|
||||
|
||||
// Example of value:
|
||||
|
|
@ -69,9 +69,16 @@ fun NotifiableEvent.shouldIgnoreEventInRoom(appNavigationState: AppNavigationSta
|
|||
val currentSessionId = appNavigationState.navigationState.currentSessionId() ?: return false
|
||||
return when (val currentRoomId = appNavigationState.navigationState.currentRoomId()) {
|
||||
null -> false
|
||||
else -> appNavigationState.isInForeground &&
|
||||
sessionId == currentSessionId &&
|
||||
roomId == currentRoomId &&
|
||||
(this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId()
|
||||
else -> {
|
||||
// Never ignore ringing call notifications
|
||||
if (this is NotifiableRingingCallEvent) {
|
||||
false
|
||||
} else {
|
||||
appNavigationState.isInForeground &&
|
||||
sessionId == currentSessionId &&
|
||||
roomId == currentRoomId &&
|
||||
(this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.model
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
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.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import java.time.Instant
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
data class NotifiableRingingCallEvent(
|
||||
override val sessionId: SessionId,
|
||||
override val roomId: RoomId,
|
||||
override val eventId: EventId,
|
||||
override val editedEventId: EventId?,
|
||||
override val description: String?,
|
||||
override val canBeReplaced: Boolean,
|
||||
override val isRedacted: Boolean,
|
||||
override val isUpdated: Boolean,
|
||||
val roomName: String?,
|
||||
val senderId: UserId,
|
||||
val senderDisambiguatedDisplayName: String?,
|
||||
val senderAvatarUrl: String?,
|
||||
val roomAvatarUrl: String? = null,
|
||||
val callNotifyType: CallNotifyType,
|
||||
val timestamp: Long,
|
||||
) : NotifiableEvent {
|
||||
companion object {
|
||||
fun shouldRing(callNotifyType: CallNotifyType, timestamp: Long): Boolean {
|
||||
val timeout = 10.seconds.inWholeMilliseconds
|
||||
val elapsed = Instant.now().toEpochMilli() - timestamp
|
||||
// Only ring if the type is RING and the elapsed time is less than the timeout
|
||||
return callNotifyType == CallNotifyType.RING && elapsed < timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,11 +17,15 @@
|
|||
package io.element.android.libraries.push.impl.push
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
||||
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
|
|
@ -44,6 +48,8 @@ class DefaultPushHandler @Inject constructor(
|
|||
private val buildMeta: BuildMeta,
|
||||
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||
private val diagnosticPushHandler: DiagnosticPushHandler,
|
||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||
private val notificationChannels: NotificationChannels,
|
||||
) : PushHandler {
|
||||
/**
|
||||
* Called when message is received.
|
||||
|
|
@ -91,19 +97,33 @@ class DefaultPushHandler @Inject constructor(
|
|||
return
|
||||
}
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(userId)
|
||||
if (userPushStore.getNotificationEnabledForDevice().first()) {
|
||||
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
||||
if (areNotificationsEnabled) {
|
||||
val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
|
||||
if (notifiableEvent == null) {
|
||||
Timber.w("Unable to get a notification data")
|
||||
return
|
||||
when (notifiableEvent) {
|
||||
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
|
||||
is NotifiableRingingCallEvent -> handleRingingCallEvent(notifiableEvent)
|
||||
else -> onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
}
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
} else {
|
||||
// TODO We need to check if this is an incoming call
|
||||
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) {
|
||||
Timber.i("## handleInternal() : Incoming call.")
|
||||
elementCallEntryPoint.handleIncomingCall(
|
||||
callType = CallType.RoomCall(notifiableEvent.sessionId, notifiableEvent.roomId),
|
||||
eventId = notifiableEvent.eventId,
|
||||
senderId = notifiableEvent.senderId,
|
||||
roomName = notifiableEvent.roomName,
|
||||
senderName = notifiableEvent.senderDisambiguatedDisplayName,
|
||||
avatarUrl = notifiableEvent.roomAvatarUrl,
|
||||
timestamp = notifiableEvent.timestamp,
|
||||
notificationChannelId = notificationChannels.getChannelForIncomingCall(ring = true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<string name="notification_channel_call">"Пазваніць"</string>
|
||||
<string name="notification_channel_listening_for_events">"Праслухоўванне падзей"</string>
|
||||
<string name="notification_channel_noisy">"Шумныя апавяшчэнні"</string>
|
||||
<string name="notification_channel_ringing_calls">"Званкі"</string>
|
||||
<string name="notification_channel_silent">"Ціхія апавяшчэнні"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d паведамленне"</item>
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
<item quantity="many">"%d апавяшчэнняў"</item>
|
||||
</plurals>
|
||||
<string name="notification_fallback_content">"Апавяшчэнне"</string>
|
||||
<string name="notification_incoming_call">"Уваходны званок"</string>
|
||||
<string name="notification_inline_reply_failed">"** Не атрымалася даслаць - калі ласка, адкрыйце пакой"</string>
|
||||
<string name="notification_invitation_action_join">"Далучыцца"</string>
|
||||
<string name="notification_invitation_action_reject">"Адхіліць"</string>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<string name="notification_channel_call">"Hovor"</string>
|
||||
<string name="notification_channel_listening_for_events">"Naslouchání událostem"</string>
|
||||
<string name="notification_channel_noisy">"Hlasitá oznámení"</string>
|
||||
<string name="notification_channel_ringing_calls">"Vyzvánění hovorů"</string>
|
||||
<string name="notification_channel_silent">"Tichá oznámení"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d zpráva"</item>
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
<item quantity="other">"%d oznámení"</item>
|
||||
</plurals>
|
||||
<string name="notification_fallback_content">"Oznámení"</string>
|
||||
<string name="notification_incoming_call">"Příchozí hovor"</string>
|
||||
<string name="notification_inline_reply_failed">"** Nepodařilo se odeslat - otevřete prosím místnost"</string>
|
||||
<string name="notification_invitation_action_join">"Vstoupit"</string>
|
||||
<string name="notification_invitation_action_reject">"Odmítnout"</string>
|
||||
|
|
|
|||
49
libraries/push/impl/src/main/res/values-et/translations.xml
Normal file
49
libraries/push/impl/src/main/res/values-et/translations.xml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="notification_channel_call">"Kõne"</string>
|
||||
<string name="notification_channel_listening_for_events">"Kontrollime, kas on uusi sündmusi"</string>
|
||||
<string name="notification_channel_noisy">"Lärmakad teavitused"</string>
|
||||
<string name="notification_channel_silent">"Vaiksed teavitused"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d sõnum"</item>
|
||||
<item quantity="other">"%1$s: %2$d sõnumit"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_compat_summary_title">
|
||||
<item quantity="one">"%d teavitus"</item>
|
||||
<item quantity="other">"%d teavitust"</item>
|
||||
</plurals>
|
||||
<string name="notification_fallback_content">"Teavitus"</string>
|
||||
<string name="notification_inline_reply_failed">"** Saatmine ei õnnestunud - palun ava jututoa täisvaade"</string>
|
||||
<string name="notification_invitation_action_join">"Liitu"</string>
|
||||
<string name="notification_invitation_action_reject">"Lükka tagasi"</string>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="one">"%d kutse"</item>
|
||||
<item quantity="other">"%d kutset"</item>
|
||||
</plurals>
|
||||
<string name="notification_invite_body">"Kutse osalema vestluses"</string>
|
||||
<string name="notification_mentioned_you_body">"Mainis sind: %1$s"</string>
|
||||
<string name="notification_new_messages">"Uued sõnumid"</string>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
<item quantity="one">"%d uus sõnum"</item>
|
||||
<item quantity="other">"%d uut sõnumit"</item>
|
||||
</plurals>
|
||||
<string name="notification_room_action_quick_reply">"Kiirvastus"</string>
|
||||
<string name="notification_sender_me">"Mina"</string>
|
||||
<string name="notification_test_push_notification_content">"See ongi teavitus! Klõpsi mind!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
<item quantity="one">"%d lugemata sõnum, millele on teavitus saadetud"</item>
|
||||
<item quantity="other">"%d lugemata sõnumit, millele on teavitus saadetud"</item>
|
||||
</plurals>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s ja %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room">"%1$s jututoas %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s jututoas %2$s ning kutse jututuppa %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="one">"%d jututuba"</item>
|
||||
<item quantity="other">"%d jututuba"</item>
|
||||
</plurals>
|
||||
<string name="push_distributor_background_sync_android">"Sünkroniseerimine taustal"</string>
|
||||
<string name="push_distributor_firebase_android">"Google\'i teenused"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Google Play Services taustateenust ei leidu. Teavitused ei pruugi toimida korrektselt."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Viga: %1$s."</string>
|
||||
</resources>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
<string name="notification_channel_call">"Chamada"</string>
|
||||
<string name="notification_channel_listening_for_events">"À escuta de eventos"</string>
|
||||
<string name="notification_channel_noisy">"Notificações barulhentas"</string>
|
||||
<string name="notification_channel_ringing_calls">"Chamadas a tocar"</string>
|
||||
<string name="notification_channel_silent">"Notificações silenciosas"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d mensagem"</item>
|
||||
|
|
@ -13,6 +14,7 @@
|
|||
<item quantity="other">"%d notificações"</item>
|
||||
</plurals>
|
||||
<string name="notification_fallback_content">"Notificação"</string>
|
||||
<string name="notification_incoming_call">"Chamada recebida"</string>
|
||||
<string name="notification_inline_reply_failed">"** Falha no envio - por favor abre a sala"</string>
|
||||
<string name="notification_invitation_action_join">"Entrar"</string>
|
||||
<string name="notification_invitation_action_reject">"Rejeitar"</string>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<string name="notification_channel_call">"Позвонить"</string>
|
||||
<string name="notification_channel_listening_for_events">"Прослушивание событий"</string>
|
||||
<string name="notification_channel_noisy">"Шумные уведомления"</string>
|
||||
<string name="notification_channel_ringing_calls">"Звонки"</string>
|
||||
<string name="notification_channel_silent">"Бесшумные уведомления"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d сообщение"</item>
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
<item quantity="many">"%d уведомлений"</item>
|
||||
</plurals>
|
||||
<string name="notification_fallback_content">"Уведомление"</string>
|
||||
<string name="notification_incoming_call">"Входящий вызов"</string>
|
||||
<string name="notification_inline_reply_failed">"** Не удалось отправить - пожалуйста, откройте комнату"</string>
|
||||
<string name="notification_invitation_action_join">"Присоединиться"</string>
|
||||
<string name="notification_invitation_action_reject">"Отклонить"</string>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<string name="notification_channel_call">"Zavolať"</string>
|
||||
<string name="notification_channel_listening_for_events">"Počúvanie udalostí"</string>
|
||||
<string name="notification_channel_noisy">"Hlasité oznámenia"</string>
|
||||
<string name="notification_channel_ringing_calls">"Vyzváňanie hovorov"</string>
|
||||
<string name="notification_channel_silent">"Tiché oznámenia"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d správa"</item>
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
<item quantity="other">"%d oznámení"</item>
|
||||
</plurals>
|
||||
<string name="notification_fallback_content">"Oznámenie"</string>
|
||||
<string name="notification_incoming_call">"Prichádzajúci hovor"</string>
|
||||
<string name="notification_inline_reply_failed">"** Nepodarilo sa odoslať - prosím otvorte miestnosť"</string>
|
||||
<string name="notification_invitation_action_join">"Pripojiť sa"</string>
|
||||
<string name="notification_invitation_action_reject">"Zamietnuť"</string>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<string name="notification_channel_call">"Call"</string>
|
||||
<string name="notification_channel_listening_for_events">"Listening for events"</string>
|
||||
<string name="notification_channel_noisy">"Noisy notifications"</string>
|
||||
<string name="notification_channel_ringing_calls">"Ringing calls"</string>
|
||||
<string name="notification_channel_silent">"Silent notifications"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d message"</item>
|
||||
|
|
@ -13,6 +14,7 @@
|
|||
<item quantity="other">"%d notifications"</item>
|
||||
</plurals>
|
||||
<string name="notification_fallback_content">"Notification"</string>
|
||||
<string name="notification_incoming_call">"Incoming call"</string>
|
||||
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
|
||||
<string name="notification_invitation_action_join">"Join"</string>
|
||||
<string name="notification_invitation_action_reject">"Reject"</string>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Test
|
||||
|
|
@ -33,6 +34,8 @@ import org.robolectric.RobolectricTestRunner
|
|||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DefaultActiveNotificationsProviderTest {
|
||||
private val notificationIdProvider = NotificationIdProvider
|
||||
|
||||
@Test
|
||||
fun `getAllNotifications with no active notifications returns empty list`() {
|
||||
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = emptyList())
|
||||
|
|
@ -43,7 +46,6 @@ class DefaultActiveNotificationsProviderTest {
|
|||
|
||||
@Test
|
||||
fun `getAllNotifications with active notifications returns all`() {
|
||||
val notificationIdProvider = NotificationIdProvider()
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
|
|
@ -57,7 +59,6 @@ class DefaultActiveNotificationsProviderTest {
|
|||
|
||||
@Test
|
||||
fun `getNotificationsForSession returns only notifications for that session id`() {
|
||||
val notificationIdProvider = NotificationIdProvider()
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
|
||||
|
|
@ -71,7 +72,6 @@ class DefaultActiveNotificationsProviderTest {
|
|||
|
||||
@Test
|
||||
fun `getMembershipNotificationsForSession returns only membership notifications for that session id`() {
|
||||
val notificationIdProvider = NotificationIdProvider()
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value,),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
|
||||
|
|
@ -89,7 +89,6 @@ class DefaultActiveNotificationsProviderTest {
|
|||
|
||||
@Test
|
||||
fun `getMessageNotificationsForRoom returns only message notifications for those session and room ids`() {
|
||||
val notificationIdProvider = NotificationIdProvider()
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(
|
||||
id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID),
|
||||
|
|
@ -117,7 +116,6 @@ class DefaultActiveNotificationsProviderTest {
|
|||
|
||||
@Test
|
||||
fun `getMembershipNotificationsForRoom returns only membership notifications for those session and room ids`() {
|
||||
val notificationIdProvider = NotificationIdProvider()
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(
|
||||
id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID),
|
||||
|
|
@ -145,7 +143,6 @@ class DefaultActiveNotificationsProviderTest {
|
|||
|
||||
@Test
|
||||
fun `getSummaryNotification returns only the summary notification for that session id if it exists`() {
|
||||
val notificationIdProvider = NotificationIdProvider()
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
|
|
@ -172,7 +169,6 @@ class DefaultActiveNotificationsProviderTest {
|
|||
}
|
||||
return DefaultActiveNotificationsProvider(
|
||||
notificationManager = notificationManager,
|
||||
notificationIdProvider = NotificationIdProvider(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,15 @@ package io.element.android.libraries.push.impl.notifications
|
|||
|
||||
import android.content.Context
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
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.EventType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||
|
|
@ -48,7 +51,9 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeNotificatio
|
|||
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
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
||||
import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock
|
||||
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -58,6 +63,7 @@ import org.robolectric.RobolectricTestRunner
|
|||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DefaultNotifiableEventResolverTest {
|
||||
@Test
|
||||
|
|
@ -479,6 +485,109 @@ class DefaultNotifiableEventResolverTest {
|
|||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve CallNotify - ringing`() = runTest {
|
||||
val timestamp = DefaultSystemClock().epochMillis()
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
createNotificationData(
|
||||
content = NotificationContent.MessageLike.CallNotify(
|
||||
A_USER_ID_2,
|
||||
CallNotifyType.RING
|
||||
),
|
||||
timestamp = timestamp,
|
||||
)
|
||||
)
|
||||
)
|
||||
val expectedResult = NotifiableRingingCallEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID_2,
|
||||
roomName = null,
|
||||
editedEventId = null,
|
||||
description = "Incoming call",
|
||||
timestamp = timestamp,
|
||||
canBeReplaced = true,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
senderAvatarUrl = null,
|
||||
callNotifyType = CallNotifyType.RING,
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve CallNotify - ring but timed out displays the same as notify`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
createNotificationData(
|
||||
content = NotificationContent.MessageLike.CallNotify(
|
||||
A_USER_ID_2,
|
||||
CallNotifyType.RING
|
||||
),
|
||||
timestamp = 0L,
|
||||
)
|
||||
)
|
||||
)
|
||||
val expectedResult = NotifiableMessageEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
noisy = true,
|
||||
timestamp = 0L,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
senderId = UserId("@bob:server.org"),
|
||||
body = "☎\uFE0F Incoming call",
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
roomName = null,
|
||||
roomIsDirect = false,
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
imageUriString = null,
|
||||
type = EventType.CALL_NOTIFY,
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve CallNotify - notify`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
createNotificationData(
|
||||
content = NotificationContent.MessageLike.CallNotify(
|
||||
A_USER_ID_2,
|
||||
CallNotifyType.NOTIFY
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val expectedResult = NotifiableMessageEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
noisy = true,
|
||||
timestamp = A_TIMESTAMP,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
senderId = UserId("@bob:server.org"),
|
||||
body = "☎\uFE0F Incoming call",
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
roomName = null,
|
||||
roomIsDirect = false,
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
imageUriString = null,
|
||||
type = EventType.CALL_NOTIFY,
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve null cases`() {
|
||||
testNull(NotificationContent.MessageLike.CallAnswer)
|
||||
|
|
@ -551,6 +660,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
notificationMediaRepoFactory = notificationMediaRepoFactory,
|
||||
context = context,
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
callNotificationEventResolver = DefaultCallNotificationEventResolver(
|
||||
stringProvider = AndroidStringProvider(context.resources)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -558,6 +670,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
content: NotificationContent,
|
||||
isDirect: Boolean = false,
|
||||
hasMention: Boolean = false,
|
||||
timestamp: Long = A_TIMESTAMP,
|
||||
): NotificationData {
|
||||
return NotificationData(
|
||||
eventId = AN_EVENT_ID,
|
||||
|
|
@ -570,7 +683,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
isDirect = isDirect,
|
||||
isEncrypted = false,
|
||||
isNoisy = false,
|
||||
timestamp = A_TIMESTAMP,
|
||||
timestamp = timestamp,
|
||||
content = content,
|
||||
hasMention = hasMention,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,12 +28,13 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID
|
|||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoaderHolder
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
|
||||
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
|
||||
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
|
||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
|
|
@ -164,7 +165,7 @@ class DefaultNotificationDrawerManagerTest {
|
|||
val notificationManager = mockk<NotificationManagerCompat> {
|
||||
every { cancel(any(), any()) } returns Unit
|
||||
}
|
||||
val summaryId = NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID)
|
||||
val summaryId = NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID)
|
||||
val activeNotificationsProvider = FakeActiveNotificationsProvider(
|
||||
mutableListOf(
|
||||
mockk {
|
||||
|
|
@ -198,7 +199,6 @@ class DefaultNotificationDrawerManagerTest {
|
|||
return DefaultNotificationDrawerManager(
|
||||
notificationManager = notificationManager,
|
||||
notificationRenderer = NotificationRenderer(
|
||||
notificationIdProvider = NotificationIdProvider(),
|
||||
notificationDisplayer = DefaultNotificationDisplayer(context, NotificationManagerCompat.from(context)),
|
||||
notificationDataFactory = DefaultNotificationDataFactory(
|
||||
notificationCreator = FakeNotificationCreator(),
|
||||
|
|
@ -208,7 +208,6 @@ class DefaultNotificationDrawerManagerTest {
|
|||
stringProvider = FakeStringProvider(),
|
||||
),
|
||||
),
|
||||
notificationIdProvider = NotificationIdProvider(),
|
||||
appNavigationStateService = appNavigationStateService,
|
||||
coroutineScope = this,
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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 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.matrix.test.A_USER_NAME
|
||||
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.matrix.test.notification.aNotificationData
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||
import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver
|
||||
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DefaultOnMissedCallNotificationHandlerTest {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `addMissedCallNotification - should add missed call notification`() = runTest {
|
||||
val childScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
val dataFactory = FakeNotificationDataFactory(
|
||||
messageEventToNotificationsResult = lambdaRecorder { _, _, _ -> emptyList() }
|
||||
)
|
||||
// Create a fake matrix client provider that returns a fake matrix client with a fake notification service that returns a valid notification data
|
||||
val matrixClientProvider = FakeMatrixClientProvider(getClient = {
|
||||
val notificationService = FakeNotificationService().apply {
|
||||
givenGetNotificationResult(Result.success(aNotificationData(senderDisplayName = A_USER_NAME, senderIsNameAmbiguous = false)))
|
||||
}
|
||||
Result.success(FakeMatrixClient(notificationService = notificationService))
|
||||
})
|
||||
val defaultOnMissedCallNotificationHandler = DefaultOnMissedCallNotificationHandler(
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
defaultNotificationDrawerManager = DefaultNotificationDrawerManager(
|
||||
notificationManager = mockk(relaxed = true),
|
||||
notificationRenderer = NotificationRenderer(
|
||||
notificationDisplayer = FakeNotificationDisplayer(),
|
||||
notificationDataFactory = dataFactory,
|
||||
),
|
||||
appNavigationStateService = FakeAppNavigationStateService(),
|
||||
coroutineScope = childScope,
|
||||
matrixClientProvider = FakeMatrixClientProvider(),
|
||||
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||
activeNotificationsProvider = FakeActiveNotificationsProvider(),
|
||||
),
|
||||
callNotificationEventResolver = FakeCallNotificationEventResolver(resolveEventLambda = { _, _, _ -> aNotifiableMessageEvent() }),
|
||||
)
|
||||
|
||||
defaultOnMissedCallNotificationHandler.addMissedCallNotification(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
|
||||
runCurrent()
|
||||
|
||||
dataFactory.messageEventToNotificationsResult.assertions().isCalledOnce()
|
||||
|
||||
// Cancel the coroutine scope so the test can finish
|
||||
childScope.cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -25,8 +25,8 @@ 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.libraries.push.test.notifications.FakeImageLoader
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||
|
|
@ -212,7 +212,7 @@ fun createRoomGroupMessageCreator(
|
|||
sdkIntProvider: BuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.O),
|
||||
): RoomGroupMessageCreator {
|
||||
val context = RuntimeEnvironment.getApplication() as Context
|
||||
val bitmapLoader = NotificationBitmapLoader(
|
||||
val bitmapLoader = DefaultNotificationBitmapLoader(
|
||||
context = RuntimeEnvironment.getApplication(),
|
||||
sdkIntProvider = sdkIntProvider,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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 android.content.Intent
|
||||
|
||||
class FakeReplyMessageExtractor(
|
||||
private val result: String? = null,
|
||||
) : ReplyMessageExtractor {
|
||||
override fun getReplyMessage(intent: Intent): String? {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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 android.content.Intent
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
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.core.asEventId
|
||||
import io.element.android.libraries.matrix.api.room.Mention
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||
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.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.preferences.test.FakeSessionPreferencesStoreFactory
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived
|
||||
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class NotificationBroadcastReceiverHandlerTest {
|
||||
private val actionIds = NotificationActionIds(aBuildMeta())
|
||||
|
||||
@Test
|
||||
fun `When no sessionId, nothing happen`() = runTest {
|
||||
val sut = createNotificationBroadcastReceiverHandler()
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.join,
|
||||
sessionId = null
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test dismiss room without a roomId, nothing happen`() = runTest {
|
||||
val sut = createNotificationBroadcastReceiverHandler()
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.dismissRoom,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test dismiss room`() = runTest {
|
||||
val clearMessagesForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
|
||||
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||
clearMessagesForRoomLambda = clearMessagesForRoomLambda,
|
||||
)
|
||||
val sut = createNotificationBroadcastReceiverHandler(
|
||||
notificationDrawerManager = notificationDrawerManager
|
||||
)
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.dismissRoom,
|
||||
roomId = A_ROOM_ID,
|
||||
),
|
||||
)
|
||||
runCurrent()
|
||||
clearMessagesForRoomLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test dismiss summary`() = runTest {
|
||||
val clearAllMessagesEventsLambda = lambdaRecorder<SessionId, Unit> { _ -> }
|
||||
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||
clearAllMessagesEventsLambda = clearAllMessagesEventsLambda,
|
||||
)
|
||||
val sut = createNotificationBroadcastReceiverHandler(
|
||||
notificationDrawerManager = notificationDrawerManager
|
||||
)
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.dismissSummary,
|
||||
),
|
||||
)
|
||||
clearAllMessagesEventsLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test dismiss Invite without room`() = runTest {
|
||||
val sut = createNotificationBroadcastReceiverHandler()
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.dismissInvite,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test dismiss Invite`() = runTest {
|
||||
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
|
||||
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda,
|
||||
)
|
||||
val sut = createNotificationBroadcastReceiverHandler(
|
||||
notificationDrawerManager = notificationDrawerManager
|
||||
)
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.dismissInvite,
|
||||
roomId = A_ROOM_ID,
|
||||
),
|
||||
)
|
||||
clearMembershipNotificationForRoomLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test dismiss Event without event`() = runTest {
|
||||
val sut = createNotificationBroadcastReceiverHandler()
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.dismissEvent,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test dismiss Event`() = runTest {
|
||||
val clearEventLambda = lambdaRecorder<SessionId, EventId, Unit> { _, _ -> }
|
||||
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||
clearEventLambda = clearEventLambda,
|
||||
)
|
||||
val sut = createNotificationBroadcastReceiverHandler(
|
||||
notificationDrawerManager = notificationDrawerManager
|
||||
)
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.dismissEvent,
|
||||
eventId = AN_EVENT_ID,
|
||||
),
|
||||
)
|
||||
clearEventLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID), value(AN_EVENT_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test mark room as read without room`() = runTest {
|
||||
val sut = createNotificationBroadcastReceiverHandler()
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.markRoomRead,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test mark room as read, send public RR`() {
|
||||
testMarkRoomAsRead(
|
||||
isSendPublicReadReceiptsEnabled = true,
|
||||
expectedReceiptType = ReceiptType.READ
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test mark room as read, send private RR`() {
|
||||
testMarkRoomAsRead(
|
||||
isSendPublicReadReceiptsEnabled = false,
|
||||
expectedReceiptType = ReceiptType.READ_PRIVATE
|
||||
)
|
||||
}
|
||||
|
||||
private fun testMarkRoomAsRead(
|
||||
isSendPublicReadReceiptsEnabled: Boolean,
|
||||
expectedReceiptType: ReceiptType,
|
||||
) = runTest {
|
||||
val getLambda = lambdaRecorder<SessionId, CoroutineScope, SessionPreferencesStore> { _, _ ->
|
||||
InMemorySessionPreferencesStore(
|
||||
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled
|
||||
)
|
||||
}
|
||||
val sessionPreferencesStore = FakeSessionPreferencesStoreFactory(
|
||||
getLambda = getLambda
|
||||
)
|
||||
val clearMessagesForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
|
||||
val matrixRoom = FakeMatrixRoom()
|
||||
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||
clearMessagesForRoomLambda = clearMessagesForRoomLambda,
|
||||
)
|
||||
val sut = createNotificationBroadcastReceiverHandler(
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
matrixRoom = matrixRoom,
|
||||
notificationDrawerManager = notificationDrawerManager
|
||||
)
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.markRoomRead,
|
||||
roomId = A_ROOM_ID,
|
||||
),
|
||||
)
|
||||
runCurrent()
|
||||
clearMessagesForRoomLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||
assertThat(matrixRoom.markAsReadCalls).isEqualTo(listOf(expectedReceiptType))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test join room without room`() = runTest {
|
||||
val sut = createNotificationBroadcastReceiverHandler()
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.join,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test join room`() = runTest {
|
||||
val joinRoom = lambdaRecorder<RoomId, Result<Unit>> { _ -> Result.success(Unit) }
|
||||
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
|
||||
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda,
|
||||
)
|
||||
val sut = createNotificationBroadcastReceiverHandler(
|
||||
joinRoom = joinRoom,
|
||||
notificationDrawerManager = notificationDrawerManager,
|
||||
)
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.join,
|
||||
roomId = A_ROOM_ID,
|
||||
),
|
||||
)
|
||||
runCurrent()
|
||||
joinRoom.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_ROOM_ID))
|
||||
clearMembershipNotificationForRoomLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test reject room without room`() = runTest {
|
||||
val sut = createNotificationBroadcastReceiverHandler()
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.reject,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test reject room`() = runTest {
|
||||
val leaveRoom = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||
val matrixRoom = FakeMatrixRoom().apply {
|
||||
leaveRoomLambda = leaveRoom
|
||||
}
|
||||
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
|
||||
val notificationDrawerManager = FakeNotificationDrawerManager(
|
||||
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda,
|
||||
)
|
||||
val sut = createNotificationBroadcastReceiverHandler(
|
||||
matrixRoom = matrixRoom,
|
||||
notificationDrawerManager = notificationDrawerManager
|
||||
)
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.reject,
|
||||
roomId = A_ROOM_ID,
|
||||
),
|
||||
)
|
||||
runCurrent()
|
||||
clearMembershipNotificationForRoomLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||
leaveRoom.assertions()
|
||||
.isCalledOnce()
|
||||
.with()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test send reply without room`() = runTest {
|
||||
val sut = createNotificationBroadcastReceiverHandler()
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.smartReply,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test send reply`() = runTest {
|
||||
val sendMessage = lambdaRecorder<String, String?, List<Mention>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val replyMessage = lambdaRecorder<EventId, String, String?, List<Mention>, Boolean, Result<Unit>> { _, _, _, _, _ -> Result.success(Unit) }
|
||||
val liveTimeline = FakeTimeline().apply {
|
||||
sendMessageLambda = sendMessage
|
||||
replyMessageLambda = replyMessage
|
||||
}
|
||||
val matrixRoom = FakeMatrixRoom(
|
||||
liveTimeline = liveTimeline
|
||||
)
|
||||
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> }
|
||||
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult)
|
||||
val sut = createNotificationBroadcastReceiverHandler(
|
||||
matrixRoom = matrixRoom,
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
replyMessageExtractor = FakeReplyMessageExtractor(A_MESSAGE)
|
||||
)
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.smartReply,
|
||||
roomId = A_ROOM_ID,
|
||||
),
|
||||
)
|
||||
runCurrent()
|
||||
sendMessage.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_MESSAGE), value(null), value(emptyList<Mention>()))
|
||||
onNotifiableEventReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
replyMessage.assertions()
|
||||
.isNeverCalled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test send reply blank message`() = runTest {
|
||||
val sendMessage = lambdaRecorder<String, String?, List<Mention>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val liveTimeline = FakeTimeline().apply {
|
||||
sendMessageLambda = sendMessage
|
||||
}
|
||||
val matrixRoom = FakeMatrixRoom(
|
||||
liveTimeline = liveTimeline
|
||||
)
|
||||
val sut = createNotificationBroadcastReceiverHandler(
|
||||
matrixRoom = matrixRoom,
|
||||
replyMessageExtractor = FakeReplyMessageExtractor(" "),
|
||||
)
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.smartReply,
|
||||
roomId = A_ROOM_ID,
|
||||
),
|
||||
)
|
||||
runCurrent()
|
||||
sendMessage.assertions()
|
||||
.isNeverCalled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test send reply to thread`() = runTest {
|
||||
val sendMessage = lambdaRecorder<String, String?, List<Mention>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val replyMessage = lambdaRecorder<EventId, String, String?, List<Mention>, Boolean, Result<Unit>> { _, _, _, _, _ -> Result.success(Unit) }
|
||||
val liveTimeline = FakeTimeline().apply {
|
||||
sendMessageLambda = sendMessage
|
||||
replyMessageLambda = replyMessage
|
||||
}
|
||||
val matrixRoom = FakeMatrixRoom(
|
||||
liveTimeline = liveTimeline
|
||||
)
|
||||
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> }
|
||||
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult)
|
||||
val sut = createNotificationBroadcastReceiverHandler(
|
||||
matrixRoom = matrixRoom,
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
replyMessageExtractor = FakeReplyMessageExtractor(A_MESSAGE)
|
||||
)
|
||||
sut.onReceive(
|
||||
createIntent(
|
||||
action = actionIds.smartReply,
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = A_THREAD_ID,
|
||||
),
|
||||
)
|
||||
runCurrent()
|
||||
sendMessage.assertions()
|
||||
.isNeverCalled()
|
||||
onNotifiableEventReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
replyMessage.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_THREAD_ID.asEventId()), value(A_MESSAGE), value(null), value(emptyList<Mention>()), value(true))
|
||||
}
|
||||
|
||||
private fun createIntent(
|
||||
action: String,
|
||||
sessionId: SessionId? = A_SESSION_ID,
|
||||
roomId: RoomId? = null,
|
||||
eventId: EventId? = null,
|
||||
threadId: ThreadId? = null,
|
||||
) = Intent(action).apply {
|
||||
putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId?.value)
|
||||
putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId?.value)
|
||||
putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, threadId?.value)
|
||||
putExtra(NotificationBroadcastReceiver.KEY_EVENT_ID, eventId?.value)
|
||||
}
|
||||
|
||||
private fun TestScope.createNotificationBroadcastReceiverHandler(
|
||||
matrixRoom: FakeMatrixRoom? = FakeMatrixRoom(),
|
||||
joinRoom: (RoomId) -> Result<Unit> = { lambdaError() },
|
||||
matrixClient: MatrixClient? = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, matrixRoom)
|
||||
joinRoomLambda = joinRoom
|
||||
},
|
||||
sessionPreferencesStore: SessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(),
|
||||
notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager(),
|
||||
systemClock: SystemClock = FakeSystemClock(),
|
||||
onNotifiableEventReceived: OnNotifiableEventReceived = FakeOnNotifiableEventReceived(),
|
||||
stringProvider: StringProvider = FakeStringProvider(),
|
||||
replyMessageExtractor: ReplyMessageExtractor = FakeReplyMessageExtractor(),
|
||||
): NotificationBroadcastReceiverHandler {
|
||||
return NotificationBroadcastReceiverHandler(
|
||||
appCoroutineScope = this,
|
||||
matrixClientProvider = FakeMatrixClientProvider {
|
||||
if (matrixClient == null) {
|
||||
Result.failure(Exception("No matrix client"))
|
||||
} else {
|
||||
Result.success(matrixClient)
|
||||
}
|
||||
},
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
notificationDrawerManager = notificationDrawerManager,
|
||||
actionIds = actionIds,
|
||||
systemClock = systemClock,
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
stringProvider = stringProvider,
|
||||
replyMessageExtractor = replyMessageExtractor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,13 +23,13 @@ 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.FakeActiveNotificationsProvider
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
|
||||
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
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
|
||||
import io.element.android.libraries.push.test.notifications.FakeImageLoader
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -37,6 +37,7 @@ 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)
|
||||
|
|
|
|||
|
|
@ -19,12 +19,13 @@ package io.element.android.libraries.push.impl.notifications
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import org.junit.Test
|
||||
|
||||
class NotificationIdProviderTest {
|
||||
@Test
|
||||
fun `test notification id provider`() {
|
||||
val sut = NotificationIdProvider()
|
||||
val sut = NotificationIdProvider
|
||||
val offsetForASessionId = 305_410
|
||||
assertThat(sut.getSummaryNotificationId(A_SESSION_ID)).isEqualTo(offsetForASessionId + 0)
|
||||
assertThat(sut.getRoomMessagesNotificationId(A_SESSION_ID)).isEqualTo(offsetForASessionId + 1)
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ 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.api.notifications.NotificationIdProvider
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
|
||||
|
|
@ -31,6 +31,7 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiable
|
|||
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.test.notifications.FakeImageLoader
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
|
|
@ -61,10 +62,9 @@ class NotificationRendererTest {
|
|||
activeNotificationsProvider = FakeActiveNotificationsProvider(),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
private val notificationIdProvider = NotificationIdProvider()
|
||||
private val notificationIdProvider = NotificationIdProvider
|
||||
|
||||
private val notificationRenderer = NotificationRenderer(
|
||||
notificationIdProvider = notificationIdProvider,
|
||||
notificationDisplayer = notificationDisplayer,
|
||||
notificationDataFactory = notificationDataFactory,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.channels
|
||||
|
||||
class FakeNotificationChannels(
|
||||
var channelForIncomingCall: (ring: Boolean) -> String = { _ -> "" },
|
||||
var channelIdForMessage: (noisy: Boolean) -> String = { _ -> "" },
|
||||
var channelIdForTest: () -> String = { "" }
|
||||
) : NotificationChannels {
|
||||
override fun getChannelForIncomingCall(ring: Boolean): String {
|
||||
return channelForIncomingCall(ring)
|
||||
}
|
||||
|
||||
override fun getChannelIdForMessage(noisy: Boolean): String {
|
||||
return channelIdForMessage(noisy)
|
||||
}
|
||||
|
||||
override fun getChannelIdForTest(): String {
|
||||
return channelIdForTest()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.channels
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class NotificationChannelsTest {
|
||||
@Test
|
||||
@Config(sdk = [Build.VERSION_CODES.O])
|
||||
fun `init - creates notification channels and migrates old ones`() {
|
||||
val notificationManager = mockk<NotificationManagerCompat>(relaxed = true) {
|
||||
every { notificationChannels } returns emptyList()
|
||||
}
|
||||
|
||||
createNotificationChannels(notificationManager = notificationManager)
|
||||
|
||||
verify { notificationManager.createNotificationChannel(any<NotificationChannelCompat>()) }
|
||||
verify { notificationManager.createNotificationChannel(any<NotificationChannel>()) }
|
||||
verify { notificationManager.deleteNotificationChannel(any<String>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getChannelForIncomingCall - returns the right channel`() {
|
||||
val notificationChannels = createNotificationChannels()
|
||||
|
||||
val ringingChannel = notificationChannels.getChannelForIncomingCall(ring = true)
|
||||
assertThat(ringingChannel).isEqualTo(RINGING_CALL_NOTIFICATION_CHANNEL_ID)
|
||||
|
||||
val normalChannel = notificationChannels.getChannelForIncomingCall(ring = false)
|
||||
assertThat(normalChannel).isEqualTo(CALL_NOTIFICATION_CHANNEL_ID_V3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getChannelIdForMessage - returns the right channel`() {
|
||||
val notificationChannels = createNotificationChannels()
|
||||
|
||||
assertThat(notificationChannels.getChannelIdForMessage(noisy = true)).isEqualTo(NOISY_NOTIFICATION_CHANNEL_ID)
|
||||
assertThat(notificationChannels.getChannelIdForMessage(noisy = false)).isEqualTo(SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getChannelIdForTest - returns the right channel`() {
|
||||
val notificationChannels = createNotificationChannels()
|
||||
|
||||
assertThat(notificationChannels.getChannelIdForTest()).isEqualTo(NOISY_NOTIFICATION_CHANNEL_ID)
|
||||
}
|
||||
|
||||
private fun createNotificationChannels(
|
||||
notificationManager: NotificationManagerCompat = mockk(relaxed = true),
|
||||
) = DefaultNotificationChannels(
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
notificationManager = notificationManager,
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
}
|
||||
|
|
@ -29,18 +29,20 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
import io.element.android.libraries.push.impl.notifications.DefaultNotificationBitmapLoader
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationBitmapLoader
|
||||
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo
|
||||
import io.element.android.libraries.push.impl.notifications.channels.DefaultNotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.RejectInvitationActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
|
||||
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.SimpleNotifiableEvent
|
||||
import io.element.android.libraries.push.test.notifications.FakeImageLoader
|
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
||||
|
|
@ -284,7 +286,7 @@ fun createNotificationCreator(
|
|||
context: Context = RuntimeEnvironment.getApplication(),
|
||||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
notificationChannels: NotificationChannels = createNotificationChannels(),
|
||||
bitmapLoader: NotificationBitmapLoader = NotificationBitmapLoader(context, FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.R)),
|
||||
bitmapLoader: NotificationBitmapLoader = DefaultNotificationBitmapLoader(context, FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.R)),
|
||||
): NotificationCreator {
|
||||
return DefaultNotificationCreator(
|
||||
context = context,
|
||||
|
|
@ -327,5 +329,5 @@ fun createNotificationCreator(
|
|||
|
||||
fun createNotificationChannels(): NotificationChannels {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
return NotificationChannels(context, NotificationManagerCompat.from(context), FakeStringProvider(""))
|
||||
return DefaultNotificationChannels(context, NotificationManagerCompat.from(context), FakeStringProvider(""))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class FakeNotificationDataFactory(
|
|||
var inviteToNotificationsResult: LambdaOneParamRecorder<List<InviteNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
|
||||
var simpleEventToNotificationsResult: LambdaOneParamRecorder<List<SimpleNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
|
||||
var fallbackEventToNotificationsResult: LambdaOneParamRecorder<List<FallbackNotifiableEvent>, List<OneShotNotification>> =
|
||||
lambdaRecorder { _ -> emptyList() },
|
||||
lambdaRecorder { _ -> emptyList() },
|
||||
) : NotificationDataFactory {
|
||||
override suspend fun toNotifications(messages: List<NotifiableMessageEvent>, currentUser: MatrixUser, imageLoader: ImageLoader): List<RoomNotification> {
|
||||
return messageEventToNotificationsResult(messages, currentUser, imageLoader)
|
||||
|
|
@ -64,6 +64,8 @@ class FakeNotificationDataFactory(
|
|||
return simpleEventToNotificationsResult(simpleEvents)
|
||||
}
|
||||
|
||||
@JvmName("toNotificationFallbackEvents")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
override fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification> {
|
||||
return fallbackEventToNotificationsResult(fallback)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ package io.element.android.libraries.push.impl.notifications.fake
|
|||
|
||||
import android.app.Notification
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationIdProvider
|
||||
import io.element.android.tests.testutils.lambda.LambdaNoParamRecorder
|
||||
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
|
||||
import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder
|
||||
|
|
@ -51,7 +51,7 @@ class FakeNotificationDisplayer(
|
|||
|
||||
fun verifySummaryCancelled(times: Int = 1) {
|
||||
cancelNotificationMessageResult.assertions().isCalledExactly(times).withSequence(
|
||||
listOf(value(null), value(NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID)))
|
||||
listOf(value(null), value(NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID)))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,16 @@ 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.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
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.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
|
||||
fun aSimpleNotifiableEvent(
|
||||
|
|
@ -79,6 +84,7 @@ fun aNotifiableMessageEvent(
|
|||
threadId: ThreadId? = null,
|
||||
isRedacted: Boolean = false,
|
||||
timestamp: Long = 0,
|
||||
type: String = EventType.MESSAGE,
|
||||
) = NotifiableMessageEvent(
|
||||
sessionId = sessionId,
|
||||
eventId = eventId,
|
||||
|
|
@ -94,5 +100,34 @@ fun aNotifiableMessageEvent(
|
|||
roomIsDirect = false,
|
||||
canBeReplaced = false,
|
||||
isRedacted = isRedacted,
|
||||
imageUriString = null
|
||||
imageUriString = null,
|
||||
type = type,
|
||||
)
|
||||
|
||||
fun aNotifiableCallEvent(
|
||||
sessionId: SessionId = A_SESSION_ID,
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
eventId: EventId = AN_EVENT_ID,
|
||||
senderId: UserId = A_USER_ID_2,
|
||||
senderName: String? = null,
|
||||
roomAvatarUrl: String? = AN_AVATAR_URL,
|
||||
senderAvatarUrl: String? = AN_AVATAR_URL,
|
||||
callNotifyType: CallNotifyType = CallNotifyType.NOTIFY,
|
||||
timestamp: Long = 0L,
|
||||
) = NotifiableRingingCallEvent(
|
||||
sessionId = sessionId,
|
||||
eventId = eventId,
|
||||
roomId = roomId,
|
||||
roomName = "a room name",
|
||||
editedEventId = null,
|
||||
description = "description",
|
||||
timestamp = timestamp,
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
senderDisambiguatedDisplayName = senderName,
|
||||
senderId = senderId,
|
||||
roomAvatarUrl = roomAvatarUrl,
|
||||
senderAvatarUrl = senderAvatarUrl,
|
||||
callNotifyType = callNotifyType,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,11 +19,16 @@
|
|||
package io.element.android.libraries.push.impl.push
|
||||
|
||||
import app.cash.turbine.test
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.test.FakeElementCallEntryPoint
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
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.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
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_SECRET
|
||||
|
|
@ -31,6 +36,8 @@ import io.element.android.libraries.matrix.test.A_USER_ID
|
|||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
||||
|
|
@ -47,6 +54,7 @@ import io.element.android.tests.testutils.lambda.value
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
|
||||
class DefaultPushHandlerTest {
|
||||
@Test
|
||||
|
|
@ -220,6 +228,55 @@ class DefaultPushHandlerTest {
|
|||
.isNeverCalled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when ringing call PushData is received, the incoming call will be handled`() = runTest {
|
||||
val aPushData = PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
unread = 0,
|
||||
clientSecret = A_SECRET,
|
||||
)
|
||||
val handleIncomingCallLambda = lambdaRecorder<CallType.RoomCall, EventId, UserId, String?, String?, String?, String, Unit> { _, _, _, _, _, _, _ -> }
|
||||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
notifiableEventResult = { _, _, _ -> aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()) },
|
||||
incrementPushCounterResult = {},
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
)
|
||||
defaultPushHandler.handle(aPushData)
|
||||
|
||||
handleIncomingCallLambda.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when notify call PushData is received, the incoming call will be treated as a normal notification`() = runTest {
|
||||
val aPushData = PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
unread = 0,
|
||||
clientSecret = A_SECRET,
|
||||
)
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val handleIncomingCallLambda = lambdaRecorder<CallType.RoomCall, EventId, UserId, String?, String?, String?, String, Unit> { _, _, _, _, _, _, _ -> }
|
||||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = { _, _, _ -> aNotifiableMessageEvent(type = EventType.CALL_NOTIFY) },
|
||||
incrementPushCounterResult = {},
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
)
|
||||
defaultPushHandler.handle(aPushData)
|
||||
|
||||
handleIncomingCallLambda.assertions().isNeverCalled()
|
||||
onNotifiableEventReceived.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when diagnostic PushData is received, the diagnostic push handler is informed `() =
|
||||
runTest {
|
||||
|
|
@ -249,6 +306,8 @@ class DefaultPushHandlerTest {
|
|||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||
diagnosticPushHandler: DiagnosticPushHandler = DiagnosticPushHandler(),
|
||||
elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(),
|
||||
notificationChannels: FakeNotificationChannels = FakeNotificationChannels(),
|
||||
): DefaultPushHandler {
|
||||
return DefaultPushHandler(
|
||||
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived),
|
||||
|
|
@ -263,6 +322,8 @@ class DefaultPushHandlerTest {
|
|||
buildMeta = buildMeta,
|
||||
matrixAuthenticationService = matrixAuthenticationService,
|
||||
diagnosticPushHandler = diagnosticPushHandler,
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
notificationChannels = notificationChannels,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@
|
|||
package io.element.android.libraries.push.impl.push
|
||||
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeOnNotifiableEventReceived(
|
||||
private val onNotifiableEventReceivedResult: (NotifiableEvent) -> Unit,
|
||||
private val onNotifiableEventReceivedResult: (NotifiableEvent) -> Unit = { lambdaError() },
|
||||
) : OnNotifiableEventReceived {
|
||||
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
||||
onNotifiableEventReceivedResult(notifiableEvent)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,13 @@ android {
|
|||
|
||||
dependencies {
|
||||
api(projects.libraries.push.api)
|
||||
implementation(projects.libraries.push.impl)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
implementation(projects.tests.testutils)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.test)
|
||||
implementation(libs.test.robolectric)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.test.notifications
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.push.impl.notifications.CallNotificationEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
|
||||
class FakeCallNotificationEventResolver(
|
||||
var resolveEventLambda: (sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean) -> NotifiableEvent? = { _, _, _ -> null },
|
||||
) : CallNotificationEventResolver {
|
||||
override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): NotifiableEvent? {
|
||||
return resolveEventLambda(sessionId, notificationData, forceNotify)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications.fake
|
||||
package io.element.android.libraries.push.test.notifications
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
* Copyright (c) 2024 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.
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications.fake
|
||||
package io.element.android.libraries.push.test.notifications
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
|
@ -16,33 +16,36 @@
|
|||
|
||||
package io.element.android.libraries.push.test.notifications
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeNotificationDrawerManager : NotificationDrawerManager {
|
||||
private val clearMemberShipNotificationForSessionCallsCount = mutableMapOf<String, Int>()
|
||||
private val clearMemberShipNotificationForRoomCallsCount = mutableMapOf<String, Int>()
|
||||
class FakeNotificationDrawerManager(
|
||||
private val clearAllMessagesEventsLambda: (SessionId) -> Unit = { lambdaError() },
|
||||
private val clearMessagesForRoomLambda: (SessionId, RoomId) -> Unit = { _, _ -> lambdaError() },
|
||||
private val clearEventLambda: (SessionId, EventId) -> Unit = { _, _ -> lambdaError() },
|
||||
private val clearMembershipNotificationForSessionLambda: (SessionId) -> Unit = { lambdaError() },
|
||||
private val clearMembershipNotificationForRoomLambda: (SessionId, RoomId) -> Unit = { _, _ -> lambdaError() }
|
||||
) : NotificationDrawerManager {
|
||||
override fun clearAllMessagesEvents(sessionId: SessionId) {
|
||||
clearAllMessagesEventsLambda(sessionId)
|
||||
}
|
||||
|
||||
override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
clearMessagesForRoomLambda(sessionId, roomId)
|
||||
}
|
||||
|
||||
override fun clearEvent(sessionId: SessionId, eventId: EventId) {
|
||||
clearEventLambda(sessionId, eventId)
|
||||
}
|
||||
|
||||
override fun clearMembershipNotificationForSession(sessionId: SessionId) {
|
||||
clearMemberShipNotificationForSessionCallsCount.merge(sessionId.value, 1) { oldValue, value -> oldValue + value }
|
||||
clearMembershipNotificationForSessionLambda(sessionId)
|
||||
}
|
||||
|
||||
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
val key = getMembershipNotificationKey(sessionId, roomId)
|
||||
clearMemberShipNotificationForRoomCallsCount.merge(key, 1) { oldValue, value -> oldValue + value }
|
||||
}
|
||||
|
||||
fun getClearMembershipNotificationForSessionCount(sessionId: SessionId): Int {
|
||||
return clearMemberShipNotificationForRoomCallsCount[sessionId.value] ?: 0
|
||||
}
|
||||
|
||||
fun getClearMembershipNotificationForRoomCount(sessionId: SessionId, roomId: RoomId): Int {
|
||||
val key = getMembershipNotificationKey(sessionId, roomId)
|
||||
return clearMemberShipNotificationForRoomCallsCount[key] ?: 0
|
||||
}
|
||||
|
||||
private fun getMembershipNotificationKey(sessionId: SessionId, roomId: RoomId): String {
|
||||
return "$sessionId-$roomId"
|
||||
clearMembershipNotificationForRoomLambda(sessionId, roomId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.test.notifications
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
|
||||
|
||||
class FakeOnMissedCallNotificationHandler(
|
||||
var addMissedCallNotificationLambda: (SessionId, RoomId, EventId) -> Unit = { _, _, _ -> }
|
||||
) : OnMissedCallNotificationHandler {
|
||||
override suspend fun addMissedCallNotification(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
eventId: EventId,
|
||||
) {
|
||||
addMissedCallNotificationLambda(sessionId, roomId, eventId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.test.notifications.push
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import coil.ImageLoader
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
|
||||
class FakeNotificationBitmapLoader(
|
||||
var getRoomBitmapResult: (String?, ImageLoader) -> Bitmap? = { _, _ -> null },
|
||||
var getUserIconResult: (String?, ImageLoader) -> IconCompat? = { _, _ -> null },
|
||||
) : NotificationBitmapLoader {
|
||||
override suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? {
|
||||
return getRoomBitmapResult(path, imageLoader)
|
||||
}
|
||||
|
||||
override suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? {
|
||||
return getUserIconResult(path, imageLoader)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,4 +18,5 @@ package io.element.android.libraries.roomselect.api
|
|||
|
||||
enum class RoomSelectMode {
|
||||
Forward,
|
||||
Share,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,29 +31,33 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
|
|||
get() = sequenceOf(
|
||||
aRoomSelectState(),
|
||||
aRoomSelectState(query = "Test", isSearchActive = true),
|
||||
aRoomSelectState(resultState = SearchBarResultState.Results(aForwardMessagesRoomList())),
|
||||
aRoomSelectState(resultState = SearchBarResultState.Results(aRoomSelectRoomList())),
|
||||
aRoomSelectState(
|
||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
||||
resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
|
||||
query = "Test",
|
||||
isSearchActive = true,
|
||||
),
|
||||
aRoomSelectState(
|
||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
||||
resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
|
||||
query = "Test",
|
||||
isSearchActive = true,
|
||||
selectedRooms = persistentListOf(aRoomSummaryDetails(roomId = RoomId("!room2:domain")))
|
||||
),
|
||||
// Add other states here
|
||||
aRoomSelectState(
|
||||
mode = RoomSelectMode.Share,
|
||||
resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun aRoomSelectState(
|
||||
mode: RoomSelectMode = RoomSelectMode.Forward,
|
||||
resultState: SearchBarResultState<ImmutableList<RoomSummaryDetails>> = SearchBarResultState.Initial(),
|
||||
query: String = "",
|
||||
isSearchActive: Boolean = false,
|
||||
selectedRooms: ImmutableList<RoomSummaryDetails> = persistentListOf(),
|
||||
) = RoomSelectState(
|
||||
mode = RoomSelectMode.Forward,
|
||||
mode = mode,
|
||||
resultState = resultState,
|
||||
query = query,
|
||||
isSearchActive = isSearchActive,
|
||||
|
|
@ -61,7 +65,7 @@ private fun aRoomSelectState(
|
|||
eventSink = {}
|
||||
)
|
||||
|
||||
private fun aForwardMessagesRoomList() = persistentListOf(
|
||||
private fun aRoomSelectRoomList() = persistentListOf(
|
||||
aRoomSummaryDetails(),
|
||||
aRoomSummaryDetails(
|
||||
roomId = RoomId("!room2:domain"),
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ fun RoomSelectView(
|
|||
Text(
|
||||
text = when (state.mode) {
|
||||
RoomSelectMode.Forward -> stringResource(CommonStrings.common_forward_message)
|
||||
RoomSelectMode.Share -> stringResource(CommonStrings.common_send_to)
|
||||
},
|
||||
style = ElementTheme.typography.aliasScreenTitle
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="rich_text_editor_a11y_add_attachment">"Lisa manus"</string>
|
||||
<string name="rich_text_editor_bullet_list">"Lülita mummudega loend sisse/välja"</string>
|
||||
<string name="rich_text_editor_close_formatting_options">"Sulge vorminduse valikud"</string>
|
||||
<string name="rich_text_editor_code_block">"Lülita lähtekoodi lõik sisse/välja"</string>
|
||||
<string name="rich_text_editor_composer_placeholder">"Sõnum…"</string>
|
||||
<string name="rich_text_editor_create_link">"Lisa link"</string>
|
||||
<string name="rich_text_editor_edit_link">"Muuda linki"</string>
|
||||
<string name="rich_text_editor_format_bold">"Kasuta paksu kirja"</string>
|
||||
<string name="rich_text_editor_format_italic">"Kasuta kaldkirja"</string>
|
||||
<string name="rich_text_editor_format_strikethrough">"Kasuta läbikriipsutatud kirja"</string>
|
||||
<string name="rich_text_editor_format_underline">"Kasuta allajoonitud kirja"</string>
|
||||
<string name="rich_text_editor_full_screen_toggle">"Lülita täisekraanivaade sisse/välja"</string>
|
||||
<string name="rich_text_editor_indent">"Lisa taandrida"</string>
|
||||
<string name="rich_text_editor_inline_code">"Kuva lähtekoodi lõiguna"</string>
|
||||
<string name="rich_text_editor_link">"Lisa link"</string>
|
||||
<string name="rich_text_editor_numbered_list">"Lülita nummerdatud loend sisse/välja"</string>
|
||||
<string name="rich_text_editor_open_compose_options">"Ava vorminduse valikud"</string>
|
||||
<string name="rich_text_editor_quote">"Lülita tsiteerimine sisse/välja"</string>
|
||||
<string name="rich_text_editor_remove_link">"Eemalda link"</string>
|
||||
<string name="rich_text_editor_unindent">"Eemalda taandrida"</string>
|
||||
<string name="rich_text_editor_url_placeholder">"Link"</string>
|
||||
</resources>
|
||||
|
|
@ -85,6 +85,7 @@
|
|||
<string name="action_quick_reply">"Хуткі адказ"</string>
|
||||
<string name="action_quote">"Цытата"</string>
|
||||
<string name="action_react">"Рэакцыя"</string>
|
||||
<string name="action_reject">"Адхіліць"</string>
|
||||
<string name="action_remove">"Выдаліць"</string>
|
||||
<string name="action_reply">"Адказаць"</string>
|
||||
<string name="action_reply_in_thread">"Адказаць у гутаркі"</string>
|
||||
|
|
@ -162,7 +163,7 @@
|
|||
<string name="common_message_layout">"Выгляд паведамлення"</string>
|
||||
<string name="common_message_removed">"Паведамленне выдалена"</string>
|
||||
<string name="common_modern">"Сучасны"</string>
|
||||
<string name="common_mute">"Адключыць гук"</string>
|
||||
<string name="common_mute">"Адкл. гук"</string>
|
||||
<string name="common_no_results">"Вынікаў няма"</string>
|
||||
<string name="common_no_room_name">"Няма назвы пакоя"</string>
|
||||
<string name="common_offline">"Па-за сеткай"</string>
|
||||
|
|
@ -201,6 +202,7 @@
|
|||
<string name="common_search_results">"Вынікі пошуку"</string>
|
||||
<string name="common_security">"Бяспека"</string>
|
||||
<string name="common_seen_by">"Прагледжана"</string>
|
||||
<string name="common_send_to">"Адправіць"</string>
|
||||
<string name="common_sending">"Адпраўка…"</string>
|
||||
<string name="common_sending_failed">"Памылка адпраўкі"</string>
|
||||
<string name="common_sent">"Адпраўлена"</string>
|
||||
|
|
@ -226,7 +228,7 @@
|
|||
<string name="common_unable_to_invite_message">"Не ўдалося адправіць запрашэнні аднаму або некалькім карыстальнікам."</string>
|
||||
<string name="common_unable_to_invite_title">"Немагчыма адправіць запрашэнне(я)"</string>
|
||||
<string name="common_unlock">"Разблакіраваць"</string>
|
||||
<string name="common_unmute">"Уключыць гук"</string>
|
||||
<string name="common_unmute">"Укл. гук"</string>
|
||||
<string name="common_unsupported_event">"Падзея не падтрымліваецца"</string>
|
||||
<string name="common_username">"Імя карыстальніка"</string>
|
||||
<string name="common_verification_cancelled">"Праверка адменена"</string>
|
||||
|
|
@ -235,7 +237,7 @@
|
|||
<string name="common_video">"Відэа"</string>
|
||||
<string name="common_voice_message">"Галасавое паведамленне"</string>
|
||||
<string name="common_waiting">"Чакаем…"</string>
|
||||
<string name="common_waiting_for_decryption_key">"Чакаю гэтага паведамлення"</string>
|
||||
<string name="common_waiting_for_decryption_key">"Чакаю гэта паведамленне"</string>
|
||||
<string name="dialog_title_confirmation">"Пацвярджэнне"</string>
|
||||
<string name="dialog_title_error">"Памылка"</string>
|
||||
<string name="dialog_title_success">"Поспех"</string>
|
||||
|
|
@ -257,6 +259,7 @@
|
|||
<string name="invite_friends_text">"Гэй, пагавары са мной у %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Паведаміць аб памылцы з дапамогай Rageshake"</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"Уваходны званок Element Call"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Не ўдалося выбраць носьбіт, паўтарыце спробу."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз."</string>
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@
|
|||
<string name="action_quick_reply">"Rychlá odpověď"</string>
|
||||
<string name="action_quote">"Citovat"</string>
|
||||
<string name="action_react">"Reagovat"</string>
|
||||
<string name="action_reject">"Odmítnout"</string>
|
||||
<string name="action_remove">"Odstranit"</string>
|
||||
<string name="action_reply">"Odpovědět"</string>
|
||||
<string name="action_reply_in_thread">"Odpovědět ve vlákně"</string>
|
||||
|
|
@ -201,6 +202,7 @@
|
|||
<string name="common_search_results">"Výsledky hledání"</string>
|
||||
<string name="common_security">"Zabezpečení"</string>
|
||||
<string name="common_seen_by">"Viděno"</string>
|
||||
<string name="common_send_to">"Odeslat do"</string>
|
||||
<string name="common_sending">"Odesílání…"</string>
|
||||
<string name="common_sending_failed">"Odeslání se nezdařilo"</string>
|
||||
<string name="common_sent">"Odesláno"</string>
|
||||
|
|
@ -257,6 +259,7 @@
|
|||
<string name="invite_friends_text">"Ahoj, ozvi se mi na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Zatřeste zařízením pro nahlášení chyby"</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"Příchozí Element Call"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Výběr média se nezdařil, zkuste to prosím znovu."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
|
|
|
|||
262
libraries/ui-strings/src/main/res/values-et/translations.xml
Normal file
262
libraries/ui-strings/src/main/res/values-et/translations.xml
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_delete">"Kustuta"</string>
|
||||
<plurals name="a11y_digits_entered">
|
||||
<item quantity="one">"%1$d number sisestatud"</item>
|
||||
<item quantity="other">"%1$d numbrit sisestatud"</item>
|
||||
</plurals>
|
||||
<string name="a11y_hide_password">"Peida salasõna"</string>
|
||||
<string name="a11y_jump_to_bottom">"Mine lõppu"</string>
|
||||
<string name="a11y_notifications_mentions_only">"Ainult mainimised"</string>
|
||||
<string name="a11y_notifications_muted">"Summutatud"</string>
|
||||
<string name="a11y_page_n">"%1$d. lehekülg"</string>
|
||||
<string name="a11y_pause">"Peata"</string>
|
||||
<string name="a11y_pin_field">"PIN-koodi väli"</string>
|
||||
<string name="a11y_play">"Esita"</string>
|
||||
<string name="a11y_poll">"Küsitlus"</string>
|
||||
<string name="a11y_poll_end">"Lõppenud küsitlus"</string>
|
||||
<string name="a11y_react_with">"Reageeri emotikoniga %1$s"</string>
|
||||
<string name="a11y_react_with_other_emojis">"Reageeri mõne muu emotikoniga"</string>
|
||||
<string name="a11y_read_receipts_multiple">"Seda lugesid %1$s ja %2$s"</string>
|
||||
<plurals name="a11y_read_receipts_multiple_with_others">
|
||||
<item quantity="one">"Seda lugesid %1$s ja veel %2$d kasutaja"</item>
|
||||
<item quantity="other">"Seda lugesid %1$s ja veel %2$d kasutajat"</item>
|
||||
</plurals>
|
||||
<string name="a11y_read_receipts_single">"Seda luges %1$s"</string>
|
||||
<string name="a11y_read_receipts_tap_to_show_all">"Vaata kõiki"</string>
|
||||
<string name="a11y_remove_reaction_with">"Eemalda reageerimine %1$s emotikoniga"</string>
|
||||
<string name="a11y_send_files">"Saada faile"</string>
|
||||
<string name="a11y_show_password">"Näita salasõna"</string>
|
||||
<string name="a11y_start_call">"Helista"</string>
|
||||
<string name="a11y_user_menu">"Kasutajamenüü"</string>
|
||||
<string name="a11y_voice_message_record">"Salvesta häälsõnum"</string>
|
||||
<string name="a11y_voice_message_stop_recording">"Lõpeta salvestamine"</string>
|
||||
<string name="action_accept">"Nõustu"</string>
|
||||
<string name="action_add_to_timeline">"Lisa ajajoonele"</string>
|
||||
<string name="action_back">"Tagasi"</string>
|
||||
<string name="action_call">"Helista kasutajale"</string>
|
||||
<string name="action_cancel">"Loobu"</string>
|
||||
<string name="action_choose_photo">"Vali foto"</string>
|
||||
<string name="action_clear">"Selge"</string>
|
||||
<string name="action_close">"Sulge"</string>
|
||||
<string name="action_complete_verification">"Tee verifitseerimine lõpuni"</string>
|
||||
<string name="action_confirm">"Kinnita"</string>
|
||||
<string name="action_continue">"Jätka"</string>
|
||||
<string name="action_copy">"Kopeeri"</string>
|
||||
<string name="action_copy_link">"Kopeeri link"</string>
|
||||
<string name="action_copy_link_to_message">"Kopeeri sõnumi link"</string>
|
||||
<string name="action_create">"Loo"</string>
|
||||
<string name="action_create_a_room">"Loo jututuba"</string>
|
||||
<string name="action_decline">"Keeldu"</string>
|
||||
<string name="action_delete_poll">"Kustuta küsitlus"</string>
|
||||
<string name="action_disable">"Lülita välja"</string>
|
||||
<string name="action_discard">"Loobu"</string>
|
||||
<string name="action_done">"Valmis"</string>
|
||||
<string name="action_edit">"Muuda"</string>
|
||||
<string name="action_edit_poll">"Muuda küsitlust"</string>
|
||||
<string name="action_enable">"Võta kasutusele"</string>
|
||||
<string name="action_end_poll">"Lõpeta küsitlus"</string>
|
||||
<string name="action_enter_pin">"Sisesta PIN-kood"</string>
|
||||
<string name="action_forgot_password">"Kas unustasid salasõna?"</string>
|
||||
<string name="action_forward">"Edasta"</string>
|
||||
<string name="action_go_back">"Tagasi eelmisesse vaatesse"</string>
|
||||
<string name="action_invite">"Kutsu"</string>
|
||||
<string name="action_invite_friends">"Kutsu osalejaid"</string>
|
||||
<string name="action_invite_people_to_app">"Kutsu huvilisi kasutama rakendust %1$s"</string>
|
||||
<string name="action_invites_list">"Kutsed"</string>
|
||||
<string name="action_join">"Liitu"</string>
|
||||
<string name="action_learn_more">"Lisateave"</string>
|
||||
<string name="action_leave">"Lahku"</string>
|
||||
<string name="action_leave_conversation">"Lahku vestlusest"</string>
|
||||
<string name="action_leave_room">"Lahku jututoast"</string>
|
||||
<string name="action_load_more">"Näita veel"</string>
|
||||
<string name="action_manage_account">"Halda kasutajakontot"</string>
|
||||
<string name="action_manage_devices">"Halda seadmeid"</string>
|
||||
<string name="action_message">"Saada sõnum kasutajale"</string>
|
||||
<string name="action_next">"Edasi"</string>
|
||||
<string name="action_no">"Ei"</string>
|
||||
<string name="action_not_now">"Mitte praegu"</string>
|
||||
<string name="action_ok">"OK"</string>
|
||||
<string name="action_open_settings">"Seadistused"</string>
|
||||
<string name="action_open_with">"Ava rakendusega"</string>
|
||||
<string name="action_quick_reply">"Kiirvastus"</string>
|
||||
<string name="action_quote">"Tsiteeri"</string>
|
||||
<string name="action_react">"Reageeri"</string>
|
||||
<string name="action_remove">"Eemalda"</string>
|
||||
<string name="action_reply">"Vasta"</string>
|
||||
<string name="action_reply_in_thread">"Vasta jutulõngas"</string>
|
||||
<string name="action_report_bug">"Teata veast"</string>
|
||||
<string name="action_report_content">"Teata sisust haldurile"</string>
|
||||
<string name="action_reset">"Lähtesta"</string>
|
||||
<string name="action_retry">"Proovi uuesti"</string>
|
||||
<string name="action_retry_decryption">"Proovi dekrüptimist uuesti"</string>
|
||||
<string name="action_save">"Salvesta"</string>
|
||||
<string name="action_search">"Otsi"</string>
|
||||
<string name="action_send">"Saada"</string>
|
||||
<string name="action_send_message">"Saada sõnum"</string>
|
||||
<string name="action_share">"Jaga"</string>
|
||||
<string name="action_share_link">"Jaga linki"</string>
|
||||
<string name="action_sign_in_again">"Logi uuesti sisse"</string>
|
||||
<string name="action_signout">"Logi välja"</string>
|
||||
<string name="action_signout_anyway">"Ikkagi logi välja"</string>
|
||||
<string name="action_skip">"Jäta vahele"</string>
|
||||
<string name="action_start">"Alusta"</string>
|
||||
<string name="action_start_chat">"Alusta vestlust"</string>
|
||||
<string name="action_start_verification">"Alusta verifitseerimist"</string>
|
||||
<string name="action_static_map_load">"Kaardi laadimiseks klõpsa"</string>
|
||||
<string name="action_take_photo">"Tee pilt"</string>
|
||||
<string name="action_tap_for_options">"Valikuteks klõpsa"</string>
|
||||
<string name="action_try_again">"Proovi uuesti"</string>
|
||||
<string name="action_view_source">"Vaata lähtekoodi"</string>
|
||||
<string name="action_yes">"Jah"</string>
|
||||
<string name="common_about">"Rakenduse teave"</string>
|
||||
<string name="common_acceptable_use_policy">"Vastuvõetava kasutamise põhimõtted"</string>
|
||||
<string name="common_advanced_settings">"Täiendavad seadistused"</string>
|
||||
<string name="common_analytics">"Analüütika"</string>
|
||||
<string name="common_appearance">"Välimus"</string>
|
||||
<string name="common_audio">"Heli"</string>
|
||||
<string name="common_blocked_users">"Blokeeritud kasutajad"</string>
|
||||
<string name="common_bubbles">"Mullid"</string>
|
||||
<string name="common_call_invite">"Kõne on pooleli (pole toetatud)"</string>
|
||||
<string name="common_call_started">"Kõne algas"</string>
|
||||
<string name="common_chat_backup">"Vestluse varukoopia"</string>
|
||||
<string name="common_copyright">"Autoriõigused"</string>
|
||||
<string name="common_creating_room">"Loome jututoa…"</string>
|
||||
<string name="common_current_user_left_room">"Lahkus jututoast"</string>
|
||||
<string name="common_dark">"Tume"</string>
|
||||
<string name="common_decryption_error">"Dekrüptimisviga"</string>
|
||||
<string name="common_developer_options">"Arendaja valikud"</string>
|
||||
<string name="common_direct_chat">"Otsevestlus"</string>
|
||||
<string name="common_edited_suffix">"(muudetud)"</string>
|
||||
<string name="common_editing">"Muutmine"</string>
|
||||
<string name="common_emote">"* %1$s %2$s"</string>
|
||||
<string name="common_encryption_enabled">"Krüptimine on kasutusel"</string>
|
||||
<string name="common_enter_your_pin">"Sisesta oma PIN-kood"</string>
|
||||
<string name="common_error">"Viga"</string>
|
||||
<string name="common_everyone">"Kõik"</string>
|
||||
<string name="common_failed">"Ei õnnestunud"</string>
|
||||
<string name="common_favourite">"Lemmik"</string>
|
||||
<string name="common_favourited">"Lemmikuks määratud"</string>
|
||||
<string name="common_file">"Fail"</string>
|
||||
<string name="common_file_saved_on_disk_android">"Fail on salvestatud kausta Allalaadimised"</string>
|
||||
<string name="common_forward_message">"Edasta sõnum"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Pilt"</string>
|
||||
<string name="common_in_reply_to">"Vastuseks kasutajale %1$s"</string>
|
||||
<string name="common_install_apk_android">"Paigalda APK-failist"</string>
|
||||
<string name="common_invite_unknown_profile">"Sellist Matrix\'i kasutajatunnust ei õnnestu leida, seega sõnumit ilmselt keegi kätte ei saa."</string>
|
||||
<string name="common_leaving_room">"On lahkumas jututoast"</string>
|
||||
<string name="common_light">"Hele"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Link on kopeeritud lõikelauale"</string>
|
||||
<string name="common_loading">"Laadime…"</string>
|
||||
<plurals name="common_member_count">
|
||||
<item quantity="one">"%1$d liige"</item>
|
||||
<item quantity="other">"%1$d liiget"</item>
|
||||
</plurals>
|
||||
<string name="common_message">"Sõnum"</string>
|
||||
<string name="common_message_actions">"Tegevused sõnumiga"</string>
|
||||
<string name="common_message_layout">"Sõnumi paigutus"</string>
|
||||
<string name="common_message_removed">"Sõnum on eemaldatud"</string>
|
||||
<string name="common_modern">"Kaasaegne"</string>
|
||||
<string name="common_mute">"Summuta"</string>
|
||||
<string name="common_no_results">"Otsingul pole tulemusi"</string>
|
||||
<string name="common_no_room_name">"Jututoal puudub nimi"</string>
|
||||
<string name="common_offline">"Võrgust väljas"</string>
|
||||
<string name="common_or">"või"</string>
|
||||
<string name="common_password">"Salasõna"</string>
|
||||
<string name="common_people">"Inimesed"</string>
|
||||
<string name="common_permalink">"Püsilink"</string>
|
||||
<string name="common_permission">"Õigus"</string>
|
||||
<string name="common_please_wait">"Palun oota…"</string>
|
||||
<string name="common_poll_end_confirmation">"Kas oled kindel, et soovid selle küsitluse lõpetada?"</string>
|
||||
<string name="common_poll_summary">"Küsitlus: %1$s"</string>
|
||||
<string name="common_poll_total_votes">"Hääli kokku: %1$s"</string>
|
||||
<string name="common_poll_undisclosed_text">"Tulemused on näha peale küsitluse lõppemist"</string>
|
||||
<plurals name="common_poll_votes_count">
|
||||
<item quantity="one">"%d hääl"</item>
|
||||
<item quantity="other">"%d häält"</item>
|
||||
</plurals>
|
||||
<string name="common_privacy_policy">"Privaatsuspoliitika"</string>
|
||||
<string name="common_reaction">"Reaktsioon"</string>
|
||||
<string name="common_reactions">"Reaktsioonid"</string>
|
||||
<string name="common_recovery_key">"Taastevõti"</string>
|
||||
<string name="common_refreshing">"Värskendame andmeid…"</string>
|
||||
<string name="common_replying_to">"Vastates kasutajale %1$s"</string>
|
||||
<string name="common_report_a_bug">"Teata veast"</string>
|
||||
<string name="common_report_a_problem">"Teata veast"</string>
|
||||
<string name="common_report_submitted">"Veateade on saadetud"</string>
|
||||
<string name="common_rich_text_editor">"Vormindatud teksti toimeti"</string>
|
||||
<string name="common_room">"Jututuba"</string>
|
||||
<string name="common_room_name">"Jututoa nimi"</string>
|
||||
<string name="common_room_name_placeholder">"näiteks sinu projekti või seltsingu nimi"</string>
|
||||
<string name="common_saved_changes">"Muudatused on salvestatud"</string>
|
||||
<string name="common_saving">"Salvestame"</string>
|
||||
<string name="common_screen_lock">"Ekraanilukk"</string>
|
||||
<string name="common_search_for_someone">"Otsi kedagi"</string>
|
||||
<string name="common_search_results">"Otsingutulemused"</string>
|
||||
<string name="common_security">"Turvalisus"</string>
|
||||
<string name="common_seen_by">"Seda nägi"</string>
|
||||
<string name="common_sending">"Saadame…"</string>
|
||||
<string name="common_sending_failed">"Saatmine ei õnnestunud"</string>
|
||||
<string name="common_sent">"Saadetud"</string>
|
||||
<string name="common_server_not_supported">"Server pole toetatud"</string>
|
||||
<string name="common_server_url">"Serveri URL"</string>
|
||||
<string name="common_settings">"Seadistused"</string>
|
||||
<string name="common_shared_location">"Jagatud asukoht"</string>
|
||||
<string name="common_signing_out">"Logime välja"</string>
|
||||
<string name="common_something_went_wrong">"Midagi läks valesti"</string>
|
||||
<string name="common_starting_chat">"Alustame vestlust…"</string>
|
||||
<string name="common_sticker">"Kleeps"</string>
|
||||
<string name="common_success">"Õnnestus"</string>
|
||||
<string name="common_suggestions">"Soovitused"</string>
|
||||
<string name="common_third_party_notices">"Kolmandate osapoolte teatised"</string>
|
||||
<string name="common_thread">"Jutulõng"</string>
|
||||
<string name="common_topic">"Teema"</string>
|
||||
<string name="common_topic_placeholder">"Mis on selle jututoa mõte?"</string>
|
||||
<string name="common_unable_to_decrypt">"Dekrüptimine ei olnud võimalik"</string>
|
||||
<string name="common_unable_to_decrypt_no_access">"Sul pole ligipääsu antud sõnumile"</string>
|
||||
<string name="common_unable_to_invite_message">"Kutset polnud võimalik saata ühele või enamale kasutajale."</string>
|
||||
<string name="common_unable_to_invite_title">"Kutse(te) saatmine ei õnnestunud"</string>
|
||||
<string name="common_unlock">"Eemalda lukustus"</string>
|
||||
<string name="common_unmute">"Lõpeta summutamine"</string>
|
||||
<string name="common_unsupported_event">"Toetamata sündmus"</string>
|
||||
<string name="common_username">"Kasutajanimi"</string>
|
||||
<string name="common_verification_cancelled">"Verifitseerimine on katkestatud"</string>
|
||||
<string name="common_verification_complete">"Verifitseerimine on tehtud"</string>
|
||||
<string name="common_verify_device">"Verifitseeri seade"</string>
|
||||
<string name="common_video">"Video"</string>
|
||||
<string name="common_voice_message">"Häälsõnum"</string>
|
||||
<string name="common_waiting">"Ootame…"</string>
|
||||
<string name="common_waiting_for_decryption_key">"Ootame selle sõnumi dekrüptimisvõtit"</string>
|
||||
<string name="dialog_title_confirmation">"Kinnitus"</string>
|
||||
<string name="dialog_title_error">"Viga"</string>
|
||||
<string name="dialog_title_success">"Õnnestus"</string>
|
||||
<string name="dialog_title_warning">"Hoiatus"</string>
|
||||
<string name="dialog_unsaved_changes_description_android">"Sinu tehtud muudatused pole veel salvestatud. Kas sa oled kindel, et soovid minna tagasi?"</string>
|
||||
<string name="dialog_unsaved_changes_title">"Kas salvestame muudatused?"</string>
|
||||
<string name="error_failed_creating_the_permalink">"Püsilingi loomine ei õnnestumud"</string>
|
||||
<string name="error_failed_loading_map">"%1$s kaardi laadimine ei õnnestunud. Palun proovi hiljem uuesti."</string>
|
||||
<string name="error_failed_loading_messages">"Sõnumite laadimine ei õnnestunud"</string>
|
||||
<string name="error_failed_locating_user">"Rakendus %1$s ei suutnud tuvastada sinu asukohta. Palun proovi hiljem uuesti."</string>
|
||||
<string name="error_failed_uploading_voice_message">"Sinu häälsõnumi üleslaadimine ei õnnestunud."</string>
|
||||
<string name="error_message_not_found">"Sõnumit ei leidu"</string>
|
||||
<string name="error_missing_location_auth_android">"Rakendusel %1$s puudub õigus sinu asukohta tuvastada. Sa saad seda lubada süsteemi seadistustest."</string>
|
||||
<string name="error_missing_location_rationale_android">"Rakendusel %1$s puudub õigus sinu asukohta tuvastada. Alljärgnevas luba vastavad õigused."</string>
|
||||
<string name="error_missing_microphone_voice_rationale_android">"Rakendusel %1$s puudub õigus sinu nutiseadme mikrofoni kasutada. Alljärgnevas luba õigused heli salvestamiseks."</string>
|
||||
<string name="error_some_messages_have_not_been_sent">"Mõned sõnumid on saatmata"</string>
|
||||
<string name="error_unknown">"Vabandust, ilmnes viga"</string>
|
||||
<string name="invite_friends_rich_title">"🔐️ Liitu minuga rakenduses %1$s"</string>
|
||||
<string name="invite_friends_text">"Hei, suhtle minuga %1$s võrgus: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Veast teatamiseks raputa nutiseadet ägedalt"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Meediafaili valimine ei õnnestunud. Palun proovi uuesti."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Meediafaili üleslaadimine ei õnnestunud. Palun proovi uuesti."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."</string>
|
||||
<string name="screen_share_location_title">"Jaga asukohta"</string>
|
||||
<string name="screen_share_my_location_action">"Jaga minu asukohta"</string>
|
||||
<string name="screen_share_this_location_action">"Jaga seda asukohta"</string>
|
||||
<string name="screen_view_location_title">"Asukoht"</string>
|
||||
<string name="settings_version_number">"Versioon: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"et"</string>
|
||||
</resources>
|
||||
|
|
@ -197,6 +197,7 @@
|
|||
<string name="common_search_results">"Résultats de la recherche"</string>
|
||||
<string name="common_security">"Sécurité"</string>
|
||||
<string name="common_seen_by">"Vu par"</string>
|
||||
<string name="common_send_to">"Envoyer vers"</string>
|
||||
<string name="common_sending">"Envoi en cours…"</string>
|
||||
<string name="common_sending_failed">"Échec de l’envoi"</string>
|
||||
<string name="common_sent">"Envoyé"</string>
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@
|
|||
<string name="action_quick_reply">"Resposta rápida"</string>
|
||||
<string name="action_quote">"Citação"</string>
|
||||
<string name="action_react">"Reagir"</string>
|
||||
<string name="action_reject">"Rejeitar"</string>
|
||||
<string name="action_remove">"Remover"</string>
|
||||
<string name="action_reply">"Responder"</string>
|
||||
<string name="action_reply_in_thread">"Responder ao tópico"</string>
|
||||
|
|
@ -197,6 +198,7 @@
|
|||
<string name="common_search_results">"Resultados da pesquisa"</string>
|
||||
<string name="common_security">"Segurança"</string>
|
||||
<string name="common_seen_by">"Vista por"</string>
|
||||
<string name="common_send_to">"Enviar para"</string>
|
||||
<string name="common_sending">"A enviar…"</string>
|
||||
<string name="common_sending_failed">"Falha no envio"</string>
|
||||
<string name="common_sent">"Enviada"</string>
|
||||
|
|
@ -253,6 +255,7 @@
|
|||
<string name="invite_friends_text">"Alô! Fala comigo na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Agita o dispositivo em fúria para comunicar um problema"</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"A receber chamada da Element "</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Falha ao selecionar multimédia, por favor tente novamente."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Falha ao processar multimédia para carregamento, por favor tente novamente."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Falhar ao carregar multimédia, por favor tente novamente."</string>
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@
|
|||
<string name="action_quick_reply">"Быстрый ответ"</string>
|
||||
<string name="action_quote">"Цитата"</string>
|
||||
<string name="action_react">"Реакция"</string>
|
||||
<string name="action_reject">"Отклонить"</string>
|
||||
<string name="action_remove">"Удалить"</string>
|
||||
<string name="action_reply">"Ответить"</string>
|
||||
<string name="action_reply_in_thread">"Ответить в теме"</string>
|
||||
|
|
@ -203,6 +204,7 @@
|
|||
<string name="common_search_results">"Результаты поиска"</string>
|
||||
<string name="common_security">"Безопасность"</string>
|
||||
<string name="common_seen_by">"Просмотрено"</string>
|
||||
<string name="common_send_to">"Отправить"</string>
|
||||
<string name="common_sending">"Отправка…"</string>
|
||||
<string name="common_sending_failed">"Сбой отправки"</string>
|
||||
<string name="common_sent">"Отправлено"</string>
|
||||
|
|
@ -259,6 +261,7 @@
|
|||
<string name="invite_friends_text">"Привет, поговори со мной по %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Встряхните устройство, чтобы сообщить об ошибке"</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"Входящий вызов Element"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Не удалось выбрать носитель, попробуйте еще раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Не удалось загрузить медиафайлы, попробуйте еще раз."</string>
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@
|
|||
<string name="action_quick_reply">"Rýchla odpoveď"</string>
|
||||
<string name="action_quote">"Citovať"</string>
|
||||
<string name="action_react">"Reagovať"</string>
|
||||
<string name="action_reject">"Odmietnuť"</string>
|
||||
<string name="action_remove">"Odstrániť"</string>
|
||||
<string name="action_reply">"Odpovedať"</string>
|
||||
<string name="action_reply_in_thread">"Odpovedať vo vlákne"</string>
|
||||
|
|
@ -201,6 +202,7 @@
|
|||
<string name="common_search_results">"Výsledky hľadania"</string>
|
||||
<string name="common_security">"Bezpečnosť"</string>
|
||||
<string name="common_seen_by">"Videné"</string>
|
||||
<string name="common_send_to">"Odoslať"</string>
|
||||
<string name="common_sending">"Odosiela sa…"</string>
|
||||
<string name="common_sending_failed">"Odoslanie zlyhalo"</string>
|
||||
<string name="common_sent">"Odoslané"</string>
|
||||
|
|
@ -257,6 +259,7 @@
|
|||
<string name="invite_friends_text">"Ahoj, porozprávajte sa so mnou na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Zúrivo potriasť pre nahlásenie chyby"</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"Prichádzajúci hovor Element Call"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Nepodarilo sa vybrať médium, skúste to prosím znova."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Nepodarilo sa nahrať médiá, skúste to prosím znova."</string>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
<string name="action_accept">"接受"</string>
|
||||
<string name="action_add_to_timeline">"添加到时间线"</string>
|
||||
<string name="action_back">"返回"</string>
|
||||
<string name="action_call">"呼叫"</string>
|
||||
<string name="action_cancel">"取消"</string>
|
||||
<string name="action_choose_photo">"选择照片"</string>
|
||||
<string name="action_clear">"清除"</string>
|
||||
|
|
@ -70,6 +71,7 @@
|
|||
<string name="action_load_more">"载入更多"</string>
|
||||
<string name="action_manage_account">"管理账户"</string>
|
||||
<string name="action_manage_devices">"管理设备"</string>
|
||||
<string name="action_message">"发送消息给"</string>
|
||||
<string name="action_next">"下一步"</string>
|
||||
<string name="action_no">"否"</string>
|
||||
<string name="action_not_now">"以后再说"</string>
|
||||
|
|
@ -115,6 +117,7 @@
|
|||
<string name="common_blocked_users">"已屏蔽用户"</string>
|
||||
<string name="common_bubbles">"气泡"</string>
|
||||
<string name="common_call_invite">"通话进行中(不支持)"</string>
|
||||
<string name="common_call_started">"通话已开始"</string>
|
||||
<string name="common_chat_backup">"聊天记录备份"</string>
|
||||
<string name="common_copyright">"版权"</string>
|
||||
<string name="common_creating_room">"正在创建房间…"</string>
|
||||
|
|
@ -159,7 +162,7 @@
|
|||
<string name="common_offline">"离线"</string>
|
||||
<string name="common_or">"或"</string>
|
||||
<string name="common_password">"密码"</string>
|
||||
<string name="common_people">"人"</string>
|
||||
<string name="common_people">"用户"</string>
|
||||
<string name="common_permalink">"固定链接"</string>
|
||||
<string name="common_permission">"权限"</string>
|
||||
<string name="common_please_wait">"请稍候……"</string>
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@
|
|||
<string name="action_quick_reply">"Quick reply"</string>
|
||||
<string name="action_quote">"Quote"</string>
|
||||
<string name="action_react">"React"</string>
|
||||
<string name="action_reject">"Reject"</string>
|
||||
<string name="action_remove">"Remove"</string>
|
||||
<string name="action_reply">"Reply"</string>
|
||||
<string name="action_reply_in_thread">"Reply in thread"</string>
|
||||
|
|
@ -197,6 +198,7 @@
|
|||
<string name="common_search_results">"Search results"</string>
|
||||
<string name="common_security">"Security"</string>
|
||||
<string name="common_seen_by">"Seen by"</string>
|
||||
<string name="common_send_to">"Send to"</string>
|
||||
<string name="common_sending">"Sending…"</string>
|
||||
<string name="common_sending_failed">"Sending failed"</string>
|
||||
<string name="common_sent">"Sent"</string>
|
||||
|
|
@ -253,6 +255,7 @@
|
|||
<string name="invite_friends_text">"Hey, talk to me on %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake to report bug"</string>
|
||||
<string name="screen_incoming_call_subtitle_android">"Incoming Element Call"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue