Merge branch 'develop' into feature/fga/user_moderation_bottomsheet
This commit is contained in:
commit
cfbbccb09d
647 changed files with 7223 additions and 2888 deletions
|
|
@ -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">"اس کارروائی کو سنبھالنے کے لیے کوئی مطابقت پذیر اطلاقیہ نہیں ملا۔"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="common_date_date_at_time">"%1$s às %2$s"</string>
|
||||
<string name="common_date_this_month">"Este mês"</string>
|
||||
</resources>
|
||||
|
|
@ -13,9 +13,11 @@ import androidx.compose.foundation.layout.ColumnScope
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
|
|
@ -63,6 +65,7 @@ fun FlowStepPage(
|
|||
}
|
||||
},
|
||||
title = {},
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
|
||||
)
|
||||
},
|
||||
header = {
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ package io.element.android.libraries.designsystem.atomic.pages
|
|||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
|
|
@ -94,17 +96,19 @@ fun HeaderFooterPage(
|
|||
.run {
|
||||
if (isScrollable) {
|
||||
verticalScroll(rememberScrollState())
|
||||
// Make sure the scrollable content takes the full available height
|
||||
.height(IntrinsicSize.Max)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
}
|
||||
// Apply insets here so if the content is scrollable it can get below the top app bar if needed
|
||||
.padding(contentInsetsPadding)
|
||||
.weight(1f),
|
||||
.weight(1f, fill = true),
|
||||
) {
|
||||
// Header
|
||||
header()
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
Box {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
|
@ -112,7 +116,7 @@ fun HeaderFooterPage(
|
|||
// Footer
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(footerInsetsPadding)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -2,72 +2,72 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="state_event_avatar_changed_too">"(Avatar wurde auch geändert)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s hat den Avatar geändert"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Du hast deinen Avatar geändert"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Sie änderten ihren Avatar"</string>
|
||||
<string name="state_event_demoted_to_member">"%1$s wurde zum Mitglied herabgestuft"</string>
|
||||
<string name="state_event_demoted_to_moderator">"%1$s wurde zum Moderator herabgestuft"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s hat den Anzeigenamen von %2$s auf %3$s geändert"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Du hast deinen Anzeigenamen von %1$s auf %2$s geändert"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s hat den Anzeigenamen entfernt (war %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Du hast deinen Anzeigenamen entfernt (war %1$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Sie haben Ihren Anzeigenamen entfernt (er lautete %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s hat den Anzeigenamen auf %2$s geändert"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Du hast deinen Anzeigenamen zu %1$s geändert"</string>
|
||||
<string name="state_event_promoted_to_administrator">"%1$s ist jetzt Administrator*in"</string>
|
||||
<string name="state_event_promoted_to_moderator">"%1$s ist jetzt Moderator*in"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s hat den Raum-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Du hast den Raum-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Dir haben den Zimmer-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s hat den Raum-Avatar entfernt"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"Du hast den Raum-Avatar entfernt"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"Sie haben den Raum-Avatar entfernt"</string>
|
||||
<string name="state_event_room_ban">"%1$s hat %2$s gesperrt"</string>
|
||||
<string name="state_event_room_ban_by_you">"Du hast %1$s gesperrt"</string>
|
||||
<string name="state_event_room_ban_by_you_with_reason">"Sie haben gesperrt %1$s: %2$s"</string>
|
||||
<string name="state_event_room_ban_with_reason">"%1$s verboten %2$s: %3$s"</string>
|
||||
<string name="state_event_room_ban_by_you">"Sie haben %1$s gesperrt"</string>
|
||||
<string name="state_event_room_ban_by_you_with_reason">"Sie haben %1$s gesperrt: %2$s"</string>
|
||||
<string name="state_event_room_ban_with_reason">"%1$s sperrte %2$s: %3$s"</string>
|
||||
<string name="state_event_room_created">"%1$s hat den Raum erstellt"</string>
|
||||
<string name="state_event_room_created_by_you">"Du hast den Raum erstellt"</string>
|
||||
<string name="state_event_room_created_by_you">"Sie erstellten den Chatroom"</string>
|
||||
<string name="state_event_room_invite">"%1$s hat %2$s eingeladen"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s hat die Einladung angenommen"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Du hast die Einladung angenommen"</string>
|
||||
<string name="state_event_room_invite_by_you">"Du hast %1$s eingeladen"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Sie haben die Einladung angenommen"</string>
|
||||
<string name="state_event_room_invite_by_you">"Sie luden %1$s ein"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s hat dich eingeladen"</string>
|
||||
<string name="state_event_room_join">"%1$s hat den Raum betreten"</string>
|
||||
<string name="state_event_room_join_by_you">"Du hast den Raum betreten"</string>
|
||||
<string name="state_event_room_join_by_you">"Sie sind dem Raum beigetreten"</string>
|
||||
<string name="state_event_room_knock">"%1$s beantragt den Beitritt"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s hat %2$s den Beitritt erlaubt"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"Du hast %1$s den Beitritt erlaubt."</string>
|
||||
<string name="state_event_room_knock_by_you">"Du hast angefragt beizutreten"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"Sie genehmigten die Beitrittsanfrage für %1$s"</string>
|
||||
<string name="state_event_room_knock_by_you">"Sie haben um Beitritt gebeten"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s hat die Beitrittsanfrage von %2$s abgelehnt"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"Du hast die Beitrittsanfrage von %1$s abgelehnt"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"Sie haben die Beitrittsanfrage für %1$s abgelehnt"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s hat deine Beitrittsanfrage abgelehnt"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s ist nicht mehr an einem Beitritt interessiert"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"Du hast deine Beitrittsanfrage zurückgezogen"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"Sie haben ihre Beitrittsanfrage widerrufen"</string>
|
||||
<string name="state_event_room_leave">"%1$s hat den Raum verlassen"</string>
|
||||
<string name="state_event_room_leave_by_you">"Du hast den Raum verlassen"</string>
|
||||
<string name="state_event_room_leave_by_you">"Sie verließen den Raum"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s hat den Raum-Namen geändert in: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Du hast den Raum-Namen geändert in: %1$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Sie haben den Raumnamen geändert in: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s hat den Raum-Namen entfernt"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Du hast den Raum-Namen entfernt"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Sie haben den Raumnamen entfernt"</string>
|
||||
<string name="state_event_room_none">"%1$s hat keine Änderungen vorgenommen"</string>
|
||||
<string name="state_event_room_none_by_you">"Du hast keine Änderungen vorgenommen"</string>
|
||||
<string name="state_event_room_none_by_you">"Sie haben keine Änderungen vorgenommen"</string>
|
||||
<string name="state_event_room_pinned_events_changed">"%1$s hat die fixierten Nachrichten geändert"</string>
|
||||
<string name="state_event_room_pinned_events_changed_by_you">"Du hast die fixierten Nachrichten geändert"</string>
|
||||
<string name="state_event_room_pinned_events_changed_by_you">"Sie haben die angehefteten Nachrichten geändert."</string>
|
||||
<string name="state_event_room_pinned_events_pinned">"%1$s fixierte eine Nachricht"</string>
|
||||
<string name="state_event_room_pinned_events_pinned_by_you">"Du hast eine Nachricht fixiert"</string>
|
||||
<string name="state_event_room_pinned_events_pinned_by_you">"Sie haben eine Nachricht angeheftet."</string>
|
||||
<string name="state_event_room_pinned_events_unpinned">"%1$s löste eine Nachricht"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned_by_you">"Du hast eine Nachricht gelöst"</string>
|
||||
<string name="state_event_room_reject">"%1$s hat die Einladung abgelehnt"</string>
|
||||
<string name="state_event_room_reject_by_you">"Du hast die Einladung abgelehnt"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned_by_you">"Sie haben eine angeheftete Nachricht entfernt."</string>
|
||||
<string name="state_event_room_reject">"%1$s lehnte die Einladung ab"</string>
|
||||
<string name="state_event_room_reject_by_you">"Sie lehnten die Einladung ab"</string>
|
||||
<string name="state_event_room_remove">"%1$s hat %2$s entfernt"</string>
|
||||
<string name="state_event_room_remove_by_you">"Du hast %1$s entfernt"</string>
|
||||
<string name="state_event_room_remove_by_you">"Sie haben %1$s entfernt"</string>
|
||||
<string name="state_event_room_remove_by_you_with_reason">"Sie haben %1$s entfernt: %2$s"</string>
|
||||
<string name="state_event_room_remove_with_reason">"%1$s entfernt %2$s: %3$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s hat eine Einladung an %2$s gesendet, dem Raum beizutreten"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Du hast eine Einladung an %1$s gesendet, dem Raum beizutreten"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s hat %2$s eingeladen, den Raum zu beizutreten"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Sie haben eine Einladung an %1$s gesendet, um dem Chatroom beizutreten."</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s hat die Einladung an %2$s zum Betreten des Raums zurückgezogen"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Du hast die Einladung an %1$s zum Betreten des Raums zurückgezogen"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Sie haben die Einladung für %1$s widerrufen"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s hat das Thema geändert in: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Du hast das Thema geändert in: %1$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Sie haben das Thema geändert in:%1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s hat das Raum-Thema entfernt"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Du hast das Raum-Thema entfernt"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Sie haben das Raumthema entfernt"</string>
|
||||
<string name="state_event_room_unban">"%1$s hat die Sperre für %2$s aufgehoben"</string>
|
||||
<string name="state_event_room_unban_by_you">"Du hast die Sperre für %1$s aufgehoben"</string>
|
||||
<string name="state_event_room_unban_by_you">"Sie haben die Sperre für %1$s aufgehoben"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s hat eine unbekannte Änderung vorgenommen"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
<string name="state_event_room_avatar_removed_by_you">"Você removeu o avatar da sala"</string>
|
||||
<string name="state_event_room_ban">"%1$s baniu %2$s"</string>
|
||||
<string name="state_event_room_ban_by_you">"Você baniu %1$s"</string>
|
||||
<string name="state_event_room_ban_by_you_with_reason">"Você baniu %1$s: %2$s"</string>
|
||||
<string name="state_event_room_ban_with_reason">"%1$s banido %2$s: %3$s"</string>
|
||||
<string name="state_event_room_created">"%1$s criou a sala"</string>
|
||||
<string name="state_event_room_created_by_you">"Você criou a sala"</string>
|
||||
<string name="state_event_room_invite">"%1$s convidou %2$s"</string>
|
||||
|
|
@ -45,10 +47,18 @@
|
|||
<string name="state_event_room_name_removed_by_you">"Você removeu o nome da sala"</string>
|
||||
<string name="state_event_room_none">"%1$s não fez alterações"</string>
|
||||
<string name="state_event_room_none_by_you">"Você não fez nenhuma alteração"</string>
|
||||
<string name="state_event_room_pinned_events_changed">"%1$s alterou as mensagens fixadas"</string>
|
||||
<string name="state_event_room_pinned_events_changed_by_you">"Você alterou as mensagens fixadas"</string>
|
||||
<string name="state_event_room_pinned_events_pinned">"%1$s fixou uma mensagem"</string>
|
||||
<string name="state_event_room_pinned_events_pinned_by_you">"Você fixou uma mensagem"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned">"%1$s desfixou uma mensagem"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned_by_you">"Você desafixou uma mensagem"</string>
|
||||
<string name="state_event_room_reject">"%1$s rejeitou o convite"</string>
|
||||
<string name="state_event_room_reject_by_you">"Você rejeitou o convite"</string>
|
||||
<string name="state_event_room_remove">"%1$s removido %2$s"</string>
|
||||
<string name="state_event_room_remove_by_you">"Você removeu %1$s"</string>
|
||||
<string name="state_event_room_remove_by_you_with_reason">"Você removeu %1$s: %2$s"</string>
|
||||
<string name="state_event_room_remove_with_reason">"%1$s removido %2$s: %3$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s enviou um convite para %2$s para entrar na sala"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Você enviou um convite para %1$s para entrar na sala"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s revogou o convite para %2$s para entrar na sala"</string>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
<string name="state_event_room_avatar_removed_by_you">"Removeste o ícone da sala"</string>
|
||||
<string name="state_event_room_ban">"%1$s baniu %2$s"</string>
|
||||
<string name="state_event_room_ban_by_you">"Baniste %1$s"</string>
|
||||
<string name="state_event_room_ban_by_you_with_reason">"Baniste %1$s: %2$s"</string>
|
||||
<string name="state_event_room_ban_with_reason">"%1$s baniu %2$s: %3$s"</string>
|
||||
<string name="state_event_room_created">"%1$s criou a sala"</string>
|
||||
<string name="state_event_room_created_by_you">"Criaste a sala"</string>
|
||||
<string name="state_event_room_invite">"%1$s convidou %2$s"</string>
|
||||
|
|
@ -55,6 +57,8 @@
|
|||
<string name="state_event_room_reject_by_you">"Rejeitaste o convite"</string>
|
||||
<string name="state_event_room_remove">"%1$s removeu %2$s"</string>
|
||||
<string name="state_event_room_remove_by_you">"Removeste %1$s"</string>
|
||||
<string name="state_event_room_remove_by_you_with_reason">"Removeste %1$s: %2$s"</string>
|
||||
<string name="state_event_room_remove_with_reason">"%1$s removeu %2$s: %3$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s enviou um convite a %2$s"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Enviaste um convite a %1$s"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s revogou o convite de %2$s"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="state_event_avatar_changed_too">"(اوتار بھی تبدیل ہوا)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s نے اپنا اوتار بدل دیا"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"آپنے اپنا اوتار بدل دیا"</string>
|
||||
<string name="state_event_demoted_to_member">"%1$s کو تا رکن تنزل کیا گیا"</string>
|
||||
<string name="state_event_demoted_to_moderator">"%1$s کو تا ناظم تنزل کیا گیا"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s نے اپنا نمائشی نام %2$s سے %3$s پر بدل دیا"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"آپنے اپنا نمائشی نام %1$s سے %2$s پر بدل دیا"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s نے اپنا نمائشی نام ہٹادیا (%2$s تھا)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"آپنے اپنا نمائشی نام ہٹادیا (%1$s تھا)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s نے اپنا نمائشی نام %2$s پر متعین کردیا"</string>
|
||||
<string name="state_event_display_name_set_by_you">"آپنے اپنا نمائشی نام %1$s پر متعین کردیا"</string>
|
||||
<string name="state_event_promoted_to_administrator">"%1$s کو تا منتظم فروغ دیا گیا"</string>
|
||||
<string name="state_event_promoted_to_moderator">"%1$s کو تا ناظم فروغ دیا گیا"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s نے کمرے کا اوتار بدل دیا"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"آپنے کمرے کا اوتار بدل دیا"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s نے کمرے کا اوتار ہٹادیا"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"آپنے کمرے کا اوتار ہٹادیا"</string>
|
||||
<string name="state_event_room_ban">"%1$s نے %2$s پر پابندی لگادی"</string>
|
||||
<string name="state_event_room_ban_by_you">"آپنے %1$s پر پابندی لگادی"</string>
|
||||
<string name="state_event_room_created">"%1$s نے کمرہ تخلیق کیا"</string>
|
||||
<string name="state_event_room_created_by_you">"آپ نے کمرہ تخلیق کیا"</string>
|
||||
<string name="state_event_room_invite">"%1$s نے %2$s کو مدعو کیا"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s نے دعوت قبول کرلی"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"آپنے دعوت قبول کرلی"</string>
|
||||
<string name="state_event_room_invite_by_you">"آپنے %1$s مدعو کیا"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s نے آپکو مدعو کیا"</string>
|
||||
<string name="state_event_room_join">"%1$s کمرے میں شامل ہوگیا"</string>
|
||||
<string name="state_event_room_join_by_you">"آپ کمرے میں شامل ہو گئے"</string>
|
||||
<string name="state_event_room_knock">"%1$s شامل ہونے کی درخواست کر رہا ہے"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s نے %2$s کو اجازت فراہم کی"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"آپنے %1$s کو شامل ہونے کی اجازت دی"</string>
|
||||
<string name="state_event_room_knock_by_you">"آپنے شامل ہونے کی دعوت کی"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s نے %2$s کی شامل ہونے کی درخواست مسترد کردی"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"آپنے %1$s کی شامل ہونے کی درخواست مسترد کردی"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s نے آپکی شامل ہونے کی درخواست مسترد کردی"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s کی شامل ہونے میں دلچسپی نہیں"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"آپنے اپنی شامل ہونے کی درخواست منسوخ کردی"</string>
|
||||
<string name="state_event_room_leave">"%1$s کمرے سے رخصت ہوگیا"</string>
|
||||
<string name="state_event_room_leave_by_you">"آپ کمرے سے رخصت ہوگئے"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s نے کمرے کا نام بدل دیا تا: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"آپنے کمرے کا نام بدل دیا تا: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s نے کمرے کا نام ہٹادیا"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"آپنے کمرے کا نام ہٹادیا"</string>
|
||||
<string name="state_event_room_none">"%1$s نے کوئی تبدیلیاں نہیں کیں"</string>
|
||||
<string name="state_event_room_none_by_you">"آپ نے کوئی تبدیلیاں نہیں کیں"</string>
|
||||
<string name="state_event_room_pinned_events_changed">"%1$s نے مثبوتہ پیغامات بدل دیے"</string>
|
||||
<string name="state_event_room_pinned_events_changed_by_you">"آپنے مثبوتہ پیغامات بدل دیے"</string>
|
||||
<string name="state_event_room_pinned_events_pinned">"%1$s نے پیغام تثبیت کردیا"</string>
|
||||
<string name="state_event_room_pinned_events_pinned_by_you">"آپنے پیغام تثبیت کردیا"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned">"%1$s نے پیغام غیر مثبوت کردیا"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned_by_you">"آپنے پیغام غیر مثبوت کردیا"</string>
|
||||
<string name="state_event_room_reject">"%1$s نے دعوت مسترد کر دی"</string>
|
||||
<string name="state_event_room_reject_by_you">"آپنے دعوت مسترد کر دی"</string>
|
||||
<string name="state_event_room_remove">"%1$s نے %2$s کو ہٹادیا"</string>
|
||||
<string name="state_event_room_remove_by_you">"آپنے %1$s کو ہٹادیا"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s نے %2$s کو کمرے میں شامل ہونی کی دعوت بھیج دی"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"آپنے %1$s کو کمرے میں شامل ہونے کی دعوت بھیج دی"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s نے %2$s کی کمرے میں شامل ہونے کی دعوت منسوخ کردی"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"آپنے %1$s کے کمرے میں شامل ہونے کی دعوت منسوخ کردی"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s نے موضوع کو بدلا تا: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"آپنے موضوع کو بدلا تا: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s نے کمرے کا موضوع ہٹادیا"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"آپنے کمرے کا موضوع ہٹادیا"</string>
|
||||
<string name="state_event_room_unban">"%1$s کے %2$s کو غیر پابندی یافتہ کردیا"</string>
|
||||
<string name="state_event_room_unban_by_you">"آپنے %1$s کو غیر پابندی یافتہ کردیا"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s نے اپنی رکنیت میں ایک نامعلوم تبدیلی کی"</string>
|
||||
</resources>
|
||||
|
|
@ -19,6 +19,8 @@
|
|||
<string name="state_event_room_avatar_removed_by_you">"您移除了聊天室大頭照"</string>
|
||||
<string name="state_event_room_ban">"%1$s 將 %2$s 加入黑名單"</string>
|
||||
<string name="state_event_room_ban_by_you">"您將 %1$s 加入黑名單"</string>
|
||||
<string name="state_event_room_ban_by_you_with_reason">"您封鎖了 %1$s:%2$s"</string>
|
||||
<string name="state_event_room_ban_with_reason">"%1$s 封鎖了 %2$s:%3$s"</string>
|
||||
<string name="state_event_room_created">"%1$s 建立此聊天室"</string>
|
||||
<string name="state_event_room_created_by_you">"您建立此聊天室"</string>
|
||||
<string name="state_event_room_invite">"%1$s 已邀請 %2$s"</string>
|
||||
|
|
@ -55,6 +57,8 @@
|
|||
<string name="state_event_room_reject_by_you">"您拒絕了邀請"</string>
|
||||
<string name="state_event_room_remove">"%2$s 已被 %1$s 移除"</string>
|
||||
<string name="state_event_room_remove_by_you">"%1$s 已被您移除"</string>
|
||||
<string name="state_event_room_remove_by_you_with_reason">"您移除了 %1$s:%2$s"</string>
|
||||
<string name="state_event_room_remove_with_reason">"%1$s 移除了 %2$s:%3$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s 已邀請 %2$s 加入聊天室"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"您已邀請 %1$s 加入聊天室"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s 撤銷了對 %2$s 的聊天室邀請"</string>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ dependencies {
|
|||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.truth)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.indicator.impl
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultIndicatorServiceTest {
|
||||
@Test
|
||||
fun `test - showRoomListTopBarIndicator`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val sessionVerificationService = FakeSessionVerificationService()
|
||||
val sut = DefaultIndicatorService(
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
encryptionService = encryptionService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
sut.showRoomListTopBarIndicator().value
|
||||
}.test {
|
||||
assertThat(awaitItem()).isTrue()
|
||||
sessionVerificationService.emitNeedsSessionVerification(false)
|
||||
encryptionService.emitBackupState(BackupState.ENABLED)
|
||||
encryptionService.emitRecoveryState(RecoveryState.ENABLED)
|
||||
assertThat(awaitItem()).isFalse()
|
||||
sessionVerificationService.emitNeedsSessionVerification(true)
|
||||
assertThat(awaitItem()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - showSettingChatBackupIndicator is true when BackupState is UNKNOWN`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val sessionVerificationService = FakeSessionVerificationService()
|
||||
val sut = DefaultIndicatorService(
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
encryptionService = encryptionService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
sut.showSettingChatBackupIndicator().value
|
||||
}.test {
|
||||
assertThat(awaitItem()).isTrue()
|
||||
encryptionService.emitBackupState(BackupState.ENABLED)
|
||||
encryptionService.emitRecoveryState(RecoveryState.ENABLED)
|
||||
assertThat(awaitItem()).isFalse()
|
||||
encryptionService.emitBackupState(BackupState.UNKNOWN)
|
||||
assertThat(awaitItem()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - showSettingChatBackupIndicator is true when recoveryState is DISABLED`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val sessionVerificationService = FakeSessionVerificationService()
|
||||
val sut = DefaultIndicatorService(
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
encryptionService = encryptionService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
sut.showSettingChatBackupIndicator().value
|
||||
}.test {
|
||||
assertThat(awaitItem()).isTrue()
|
||||
encryptionService.emitBackupState(BackupState.ENABLED)
|
||||
encryptionService.emitRecoveryState(RecoveryState.ENABLED)
|
||||
assertThat(awaitItem()).isFalse()
|
||||
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
|
||||
assertThat(awaitItem()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test - showSettingChatBackupIndicator is true when recoveryState is INCOMPLETE`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val sessionVerificationService = FakeSessionVerificationService()
|
||||
val sut = DefaultIndicatorService(
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
encryptionService = encryptionService,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
sut.showSettingChatBackupIndicator().value
|
||||
}.test {
|
||||
assertThat(awaitItem()).isTrue()
|
||||
encryptionService.emitBackupState(BackupState.ENABLED)
|
||||
encryptionService.emitRecoveryState(RecoveryState.ENABLED)
|
||||
assertThat(awaitItem()).isFalse()
|
||||
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||
assertThat(awaitItem()).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
19
libraries/indicator/test/build.gradle.kts
Normal file
19
libraries/indicator/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.indicator.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.libraries.indicator.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.indicator.test
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import io.element.android.libraries.indicator.api.IndicatorService
|
||||
|
||||
class FakeIndicatorService : IndicatorService {
|
||||
private val showRoomListTopBarIndicatorResult: MutableState<Boolean> = mutableStateOf(false)
|
||||
private val showSettingChatBackupIndicatorResult: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
fun setShowRoomListTopBarIndicator(value: Boolean) {
|
||||
showRoomListTopBarIndicatorResult.value = value
|
||||
}
|
||||
|
||||
fun setShowSettingChatBackupIndicator(value: Boolean) {
|
||||
showSettingChatBackupIndicatorResult.value = value
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun showRoomListTopBarIndicator(): State<Boolean> {
|
||||
return showRoomListTopBarIndicatorResult
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun showSettingChatBackupIndicator(): State<Boolean> {
|
||||
return showSettingChatBackupIndicatorResult
|
||||
}
|
||||
}
|
||||
|
|
@ -9,12 +9,14 @@ package io.element.android.libraries.matrix.api.notification
|
|||
|
||||
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.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||
|
||||
data class NotificationData(
|
||||
val sessionId: SessionId,
|
||||
val eventId: EventId,
|
||||
val threadId: ThreadId?,
|
||||
val roomId: RoomId,
|
||||
|
|
|
|||
|
|
@ -11,5 +11,5 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface NotificationService {
|
||||
suspend fun getNotification(roomId: RoomId, eventId: EventId): Result<NotificationData?>
|
||||
suspend fun getNotifications(ids: Map<RoomId, List<EventId>>): Result<Map<EventId, NotificationData>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ data class RoomInfo(
|
|||
val isEncrypted: Boolean?,
|
||||
val joinRule: JoinRule?,
|
||||
val isSpace: Boolean,
|
||||
val tombstone: RoomTombstone?,
|
||||
val isFavorite: Boolean,
|
||||
val canonicalAlias: RoomAlias?,
|
||||
val alternativeAliases: ImmutableList<RoomAlias>,
|
||||
|
|
@ -74,12 +73,8 @@ data class RoomInfo(
|
|||
val pinnedEventIds: ImmutableList<EventId>,
|
||||
val creator: UserId?,
|
||||
val historyVisibility: RoomHistoryVisibility,
|
||||
val successorRoom: SuccessorRoom?,
|
||||
) {
|
||||
val aliases: List<RoomAlias>
|
||||
get() = listOfNotNull(canonicalAlias) + alternativeAliases
|
||||
}
|
||||
|
||||
data class RoomTombstone(
|
||||
val body: String,
|
||||
val replacementRoomId: RoomId,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,15 +22,12 @@ class RoomMembershipObserver {
|
|||
private val _updates = MutableSharedFlow<RoomMembershipUpdate>(extraBufferCapacity = 10)
|
||||
val updates = _updates.asSharedFlow()
|
||||
|
||||
suspend fun notifyUserLeftRoom(roomId: RoomId) {
|
||||
_updates.emit(RoomMembershipUpdate(roomId, false, MembershipChange.LEFT))
|
||||
}
|
||||
|
||||
suspend fun notifyUserDeclinedInvite(roomId: RoomId) {
|
||||
_updates.emit(RoomMembershipUpdate(roomId, false, MembershipChange.INVITATION_REJECTED))
|
||||
}
|
||||
|
||||
suspend fun notifyUserCanceledKnock(roomId: RoomId) {
|
||||
_updates.emit(RoomMembershipUpdate(roomId, false, MembershipChange.KNOCK_RETRACTED))
|
||||
suspend fun notifyUserLeftRoom(roomId: RoomId, membershipBeforeLeft: CurrentUserMembership) {
|
||||
val membershipChange = when (membershipBeforeLeft) {
|
||||
CurrentUserMembership.INVITED -> MembershipChange.INVITATION_REJECTED
|
||||
CurrentUserMembership.KNOCKED -> MembershipChange.KNOCK_RETRACTED
|
||||
else -> MembershipChange.LEFT
|
||||
}
|
||||
_updates.emit(RoomMembershipUpdate(roomId, false, membershipChange))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
/**
|
||||
*
|
||||
* When a room A is tombstoned, it is replaced by a room B. The room A is the
|
||||
* predecessor of B, and B is the successor of A. This type holds information
|
||||
* about the successor room.
|
||||
*
|
||||
* A room is tombstoned if it has received a m.room.tombstone state event.
|
||||
*
|
||||
*/
|
||||
data class SuccessorRoom(
|
||||
/**
|
||||
* The ID of the replacement room.
|
||||
*/
|
||||
val roomId: RoomId,
|
||||
/**
|
||||
* The message explaining why the room has been tombstoned.
|
||||
*/
|
||||
val reason: String?,
|
||||
)
|
||||
|
|
@ -120,10 +120,6 @@ class RustClientSessionDelegate(
|
|||
}
|
||||
}
|
||||
|
||||
override fun didRefreshTokens() {
|
||||
// This is done in `saveSessionInKeychain(Session)` instead.
|
||||
}
|
||||
|
||||
override fun retrieveSessionFromKeychain(userId: String): Session {
|
||||
// This should never be called, as it's only used for multi-process setups
|
||||
error("retrieveSessionFromKeychain should never be called for Android")
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ class RustMatrixClient(
|
|||
)
|
||||
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(innerSyncService)
|
||||
private val innerNotificationClient = runBlocking { innerClient.notificationClient(notificationProcessSetup) }
|
||||
private val notificationService = RustNotificationService(innerNotificationClient, dispatchers, clock)
|
||||
private val notificationService = RustNotificationService(sessionId, innerNotificationClient, dispatchers, clock)
|
||||
private val notificationSettingsService = RustNotificationSettingsService(innerClient, sessionCoroutineScope, dispatchers)
|
||||
private val encryptionService = RustEncryptionService(
|
||||
client = innerClient,
|
||||
|
|
@ -189,7 +189,6 @@ class RustMatrixClient(
|
|||
|
||||
private val roomMembershipObserver = RoomMembershipObserver()
|
||||
private val roomFactory = RustRoomFactory(
|
||||
innerClient = innerClient,
|
||||
roomListService = roomListService,
|
||||
innerRoomListService = innerRoomListService,
|
||||
sessionId = sessionId,
|
||||
|
|
|
|||
|
|
@ -72,8 +72,9 @@ class RustMatrixClientFactory @Inject constructor(
|
|||
suspend fun create(client: Client): RustMatrixClient {
|
||||
val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()
|
||||
|
||||
client.setUtdDelegate(UtdTracker(analyticsService))
|
||||
|
||||
val syncService = client.syncService()
|
||||
.withUtdHook(UtdTracker(analyticsService))
|
||||
.withOfflineMode()
|
||||
.finish()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.notification
|
|||
import io.element.android.libraries.core.bool.orFalse
|
||||
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.NotificationContent
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
|
|
@ -25,6 +26,7 @@ class NotificationMapper(
|
|||
private val notificationContentMapper = NotificationContentMapper()
|
||||
|
||||
fun map(
|
||||
sessionId: SessionId,
|
||||
eventId: EventId,
|
||||
roomId: RoomId,
|
||||
notificationItem: NotificationItem
|
||||
|
|
@ -35,6 +37,7 @@ class NotificationMapper(
|
|||
activeMembersCount = item.roomInfo.joinedMembersCount.toInt(),
|
||||
)
|
||||
NotificationData(
|
||||
sessionId = sessionId,
|
||||
eventId = eventId,
|
||||
// FIXME once the `NotificationItem` in the SDK returns the thread id
|
||||
threadId = null,
|
||||
|
|
|
|||
|
|
@ -10,28 +10,45 @@ package io.element.android.libraries.matrix.impl.notification
|
|||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
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.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.NotificationClient
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import org.matrix.rustcomponents.sdk.NotificationItemsRequest
|
||||
import timber.log.Timber
|
||||
|
||||
class RustNotificationService(
|
||||
private val sessionId: SessionId,
|
||||
private val notificationClient: NotificationClient,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
clock: SystemClock,
|
||||
) : NotificationService {
|
||||
private val notificationMapper: NotificationMapper = NotificationMapper(clock)
|
||||
|
||||
override suspend fun getNotification(
|
||||
roomId: RoomId,
|
||||
eventId: EventId,
|
||||
): Result<NotificationData?> = withContext(dispatchers.io) {
|
||||
override suspend fun getNotifications(
|
||||
ids: Map<RoomId, List<EventId>>
|
||||
): Result<Map<EventId, NotificationData>> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
val item = notificationClient.getNotification(roomId.value, eventId.value)
|
||||
item?.use {
|
||||
notificationMapper.map(eventId, roomId, it)
|
||||
val requests = ids.map { (roomId, eventIds) ->
|
||||
NotificationItemsRequest(
|
||||
roomId = roomId.value,
|
||||
eventIds = eventIds.map { it.value }
|
||||
)
|
||||
}
|
||||
val items = notificationClient.getNotifications(requests)
|
||||
buildMap {
|
||||
val eventIds = requests.flatMap { it.eventIds }
|
||||
for (eventId in eventIds) {
|
||||
val item = items[eventId]
|
||||
if (item != null) {
|
||||
val roomId = RoomId(requests.find { it.eventIds.contains(eventId) }?.roomId!!)
|
||||
put(EventId(eventId), notificationMapper.map(sessionId, EventId(eventId), roomId, item))
|
||||
} else {
|
||||
Timber.e("Could not retrieve event for notification with $eventId")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
package io.element.android.libraries.matrix.impl.proxy
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.getSystemService
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
|
|
@ -32,6 +34,13 @@ class DefaultProxyProvider @Inject constructor(
|
|||
private val context: Context
|
||||
) : ProxyProvider {
|
||||
override fun provides(): String? {
|
||||
val defaultProxy = context.getSystemService<ConnectivityManager>()?.defaultProxy
|
||||
if (defaultProxy == null) {
|
||||
// Note: can be tested by running:
|
||||
// adb shell settings put global http_proxy :0
|
||||
Timber.d("No default proxy")
|
||||
return null
|
||||
}
|
||||
return Settings.Global.getString(context.contentResolver, Settings.Global.HTTP_PROXY)
|
||||
?.also {
|
||||
Timber.d("Using global proxy")
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ class JoinedRustRoom(
|
|||
internalIdPrefix = internalIdPrefix,
|
||||
dateDividerMode = dateDividerMode,
|
||||
trackReadReceipts = trackReadReceipts,
|
||||
reportUtds = true,
|
||||
)
|
||||
).let { innerTimeline ->
|
||||
val mode = when (createTimelineParams) {
|
||||
|
|
@ -416,7 +417,6 @@ class JoinedRustRoom(
|
|||
RustWidgetDriver(
|
||||
widgetSettings = widgetSettings,
|
||||
room = innerRoom,
|
||||
joinedRustRoom = this,
|
||||
widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider {
|
||||
override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities {
|
||||
return getElementCallRequiredPermissions(sessionId.value, baseRoom.deviceId.value)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import io.element.android.libraries.core.coroutine.parallelMap
|
|||
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.room.ForwardEventException
|
||||
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
|
||||
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
|
||||
import io.element.android.libraries.matrix.impl.timeline.runWithTimelineListenerRegistered
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
|
@ -49,10 +48,7 @@ class RoomContentForwarder(
|
|||
val content = (messageLikeContent.kind as? MsgLikeKind.Message)?.content
|
||||
?: throw ForwardEventException(toRoomIds)
|
||||
|
||||
val targetSlidingSyncRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
|
||||
val targetRooms = targetSlidingSyncRooms.map { slidingSyncRoom ->
|
||||
slidingSyncRoom.use { it.fullRoomWithTimeline(null) }
|
||||
}
|
||||
val targetRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
|
||||
val failedForwardingTo = mutableSetOf<RoomId>()
|
||||
targetRooms.parallelMap { room ->
|
||||
room.use { targetRoom ->
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.RoomTombstone
|
||||
import io.element.android.libraries.matrix.api.room.SuccessorRoom
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.impl.room.history.map
|
||||
import io.element.android.libraries.matrix.impl.room.join.map
|
||||
|
|
@ -28,6 +28,7 @@ import uniffi.matrix_sdk_base.EncryptionState
|
|||
import org.matrix.rustcomponents.sdk.Membership as RustMembership
|
||||
import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo
|
||||
import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode
|
||||
import org.matrix.rustcomponents.sdk.SuccessorRoom as RustSuccessorRoom
|
||||
|
||||
class RoomInfoMapper {
|
||||
fun map(rustRoomInfo: RustRoomInfo): RoomInfo = rustRoomInfo.let {
|
||||
|
|
@ -47,9 +48,6 @@ class RoomInfoMapper {
|
|||
},
|
||||
joinRule = it.joinRule?.map(),
|
||||
isSpace = it.isSpace,
|
||||
tombstone = it.tombstone?.let {
|
||||
RoomTombstone(it.body, RoomId(it.replacementRoomId))
|
||||
},
|
||||
isFavorite = it.isFavourite,
|
||||
canonicalAlias = it.canonicalAlias?.let(::RoomAlias),
|
||||
alternativeAliases = it.alternativeAliases.map(::RoomAlias).toImmutableList(),
|
||||
|
|
@ -71,6 +69,7 @@ class RoomInfoMapper {
|
|||
numUnreadMentions = it.numUnreadMentions.toLong(),
|
||||
numUnreadNotifications = it.numUnreadNotifications.toLong(),
|
||||
historyVisibility = it.historyVisibility.map(),
|
||||
successorRoom = it.successorRoom?.map(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -89,6 +88,11 @@ fun RustRoomNotificationMode.map(): RoomNotificationMode = when (this) {
|
|||
RustRoomNotificationMode.MUTE -> RoomNotificationMode.MUTE
|
||||
}
|
||||
|
||||
fun RustSuccessorRoom.map(): SuccessorRoom = SuccessorRoom(
|
||||
roomId = RoomId(roomId),
|
||||
reason = reason,
|
||||
)
|
||||
|
||||
/**
|
||||
* Map a RoomHero to a MatrixUser. There is not need to create a RoomHero type on the application side.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -137,10 +137,11 @@ class RustBaseRoom(
|
|||
}
|
||||
|
||||
override suspend fun leave(): Result<Unit> = withContext(roomDispatcher) {
|
||||
val membershipBeforeLeft = roomInfoFlow.value.currentUserMembership
|
||||
runCatching {
|
||||
innerRoom.leave()
|
||||
}.onSuccess {
|
||||
roomMembershipObserver.notifyUserLeftRoom(roomId)
|
||||
roomMembershipObserver.notifyUserLeftRoom(roomId, membershipBeforeLeft)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
|||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
||||
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
|
||||
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
|
||||
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -28,10 +27,12 @@ import kotlinx.coroutines.NonCancellable
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.DateDividerMode
|
||||
import org.matrix.rustcomponents.sdk.Membership
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.TimelineConfiguration
|
||||
import org.matrix.rustcomponents.sdk.TimelineFilter
|
||||
import org.matrix.rustcomponents.sdk.TimelineFocus
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
||||
|
|
@ -39,7 +40,6 @@ import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
|||
class RustRoomFactory(
|
||||
private val sessionId: SessionId,
|
||||
private val deviceId: DeviceId,
|
||||
private val innerClient: Client,
|
||||
private val notificationSettingsService: NotificationSettingsService,
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
|
|
@ -79,16 +79,11 @@ class RustRoomFactory(
|
|||
Timber.d("Room factory is destroyed, returning null for $roomId")
|
||||
return@withContext null
|
||||
}
|
||||
val roomListItem = awaitRoomListItem(roomId) ?: return@withContext null
|
||||
getBaseRoom(roomListItem)
|
||||
val room = awaitRoomInRoomList(roomId) ?: return@withContext null
|
||||
getBaseRoom(room)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getBaseRoom(roomListItem: RoomListItem): RustBaseRoom? {
|
||||
val sdkRoom = innerClient.getRoom(roomListItem.id()) ?: return null
|
||||
return getBaseRoom(sdkRoom)
|
||||
}
|
||||
|
||||
private suspend fun getBaseRoom(sdkRoom: Room): RustBaseRoom {
|
||||
val initialRoomInfo = sdkRoom.roomInfo()
|
||||
return RustBaseRoom(
|
||||
|
|
@ -110,18 +105,27 @@ class RustRoomFactory(
|
|||
Timber.d("Room factory is destroyed, returning null for $roomId")
|
||||
return@withContext null
|
||||
}
|
||||
val roomListItem = awaitRoomListItem(roomId) ?: return@withContext null
|
||||
val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withContext null
|
||||
|
||||
if (roomListItem.membership() == Membership.JOINED) {
|
||||
// Init the live timeline in the SDK from the RoomListItem
|
||||
val sdkRoom = roomListItem.fullRoomWithTimeline(eventFilters)
|
||||
if (sdkRoom.membership() == Membership.JOINED) {
|
||||
// Init the live timeline in the SDK from the Room
|
||||
val timeline = sdkRoom.timelineWithConfiguration(
|
||||
TimelineConfiguration(
|
||||
focus = TimelineFocus.Live,
|
||||
filter = eventFilters?.let(TimelineFilter::EventTypeFilter) ?: TimelineFilter.All,
|
||||
internalIdPrefix = "live",
|
||||
dateDividerMode = DateDividerMode.DAILY,
|
||||
trackReadReceipts = true,
|
||||
reportUtds = true,
|
||||
)
|
||||
)
|
||||
|
||||
GetRoomResult.Joined(
|
||||
JoinedRustRoom(
|
||||
baseRoom = getBaseRoom(sdkRoom),
|
||||
notificationSettingsService = notificationSettingsService,
|
||||
roomContentForwarder = roomContentForwarder,
|
||||
liveInnerTimeline = sdkRoom.timeline(),
|
||||
liveInnerTimeline = timeline,
|
||||
coroutineDispatchers = dispatchers,
|
||||
systemClock = systemClock,
|
||||
featureFlagService = featureFlagService,
|
||||
|
|
@ -129,7 +133,7 @@ class RustRoomFactory(
|
|||
)
|
||||
} else {
|
||||
val preview = try {
|
||||
roomListItem.previewRoom(via = emptyList())
|
||||
sdkRoom.previewRoom(via = emptyList())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to get room preview for $roomId")
|
||||
return@withContext null
|
||||
|
|
@ -138,7 +142,7 @@ class RustRoomFactory(
|
|||
GetRoomResult.NotJoined(
|
||||
NotJoinedRustRoom(
|
||||
sessionId = sessionId,
|
||||
localRoom = getBaseRoom(roomListItem),
|
||||
localRoom = getBaseRoom(sdkRoom),
|
||||
previewInfo = RoomPreviewInfoMapper.map(preview.info()),
|
||||
)
|
||||
)
|
||||
|
|
@ -147,22 +151,22 @@ class RustRoomFactory(
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the Rust room list item for a room, retrying after the room list is loaded if necessary.
|
||||
* Get the Rust room for a room, retrying after the room list is loaded if necessary.
|
||||
*/
|
||||
private suspend fun awaitRoomListItem(roomId: RoomId): RoomListItem? {
|
||||
var roomListItem = innerRoomListService.roomOrNull(roomId.value)
|
||||
if (roomListItem == null) {
|
||||
private suspend fun awaitRoomInRoomList(roomId: RoomId): Room? {
|
||||
var sdkRoom = innerRoomListService.roomOrNull(roomId.value)
|
||||
if (sdkRoom == null) {
|
||||
// ... otherwise, lets wait for the SS to load all rooms and check again.
|
||||
roomListService.allRooms.awaitLoaded()
|
||||
roomListItem = innerRoomListService.roomOrNull(roomId.value)
|
||||
sdkRoom = innerRoomListService.roomOrNull(roomId.value)
|
||||
}
|
||||
|
||||
if (roomListItem == null) {
|
||||
if (sdkRoom == null) {
|
||||
Timber.d("Room not found for $roomId")
|
||||
return null
|
||||
}
|
||||
|
||||
return roomListItem
|
||||
return sdkRoom
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.callbackFlow
|
|||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
||||
import org.matrix.rustcomponents.sdk.RoomListInterface
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
|
||||
|
|
@ -114,7 +114,7 @@ internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncI
|
|||
)
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
internal fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? {
|
||||
internal fun RoomListServiceInterface.roomOrNull(roomId: String): Room? {
|
||||
return tryOrNull(
|
||||
onError = { Timber.e(it, "Failed finding room with id=$roomId.") }
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.roomlist
|
||||
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
|
||||
|
||||
/** Returns a `Room` with an initialized timeline using the given [filter]. */
|
||||
suspend fun RoomListItem.fullRoomWithTimeline(filter: TimelineEventTypeFilter? = null): Room {
|
||||
if (!isTimelineInitialized()) {
|
||||
initTimeline(filter, "live")
|
||||
}
|
||||
return fullRoom()
|
||||
}
|
||||
|
|
@ -10,16 +10,16 @@ package io.element.android.libraries.matrix.impl.roomlist
|
|||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.matrix.impl.room.RoomInfoMapper
|
||||
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
|
||||
class RoomSummaryFactory(
|
||||
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
|
||||
private val roomInfoMapper: RoomInfoMapper = RoomInfoMapper(),
|
||||
) {
|
||||
suspend fun create(roomListItem: RoomListItem): RoomSummary {
|
||||
val roomInfo = roomListItem.roomInfo().let(roomInfoMapper::map)
|
||||
val latestRoomMessage = roomListItem.latestEvent().use { event ->
|
||||
suspend fun create(room: Room): RoomSummary {
|
||||
val roomInfo = room.roomInfo().let(roomInfoMapper::map)
|
||||
val latestRoomMessage = room.latestEvent().use { event ->
|
||||
roomMessageFactory.create(event)
|
||||
}
|
||||
return RoomSummary(
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
|
|
@ -95,20 +95,16 @@ class RoomSummaryListProcessor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun buildSummaryForRoomListEntry(entry: RoomListItem): RoomSummary {
|
||||
return buildRoomSummaryForRoomListItem(entry)
|
||||
private suspend fun buildSummaryForRoomListEntry(entry: Room): RoomSummary {
|
||||
return entry.use { roomSummaryDetailsFactory.create(room = it) }
|
||||
}
|
||||
|
||||
private suspend fun buildRoomSummaryForIdentifier(identifier: String): RoomSummary? {
|
||||
return roomListService.roomOrNull(identifier)?.use { roomListItem ->
|
||||
buildRoomSummaryForRoomListItem(roomListItem)
|
||||
return roomListService.roomOrNull(identifier)?.let { room ->
|
||||
buildSummaryForRoomListEntry(room)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun buildRoomSummaryForRoomListItem(roomListItem: RoomListItem): RoomSummary {
|
||||
return roomSummaryDetailsFactory.create(roomListItem = roomListItem)
|
||||
}
|
||||
|
||||
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(coroutineContext) {
|
||||
mutex.withLock {
|
||||
val current = roomSummaries.replayCache.lastOrNull()
|
||||
|
|
|
|||
|
|
@ -53,4 +53,5 @@ fun TracingConfiguration.map(): org.matrix.rustcomponents.sdk.TracingConfigurati
|
|||
extraTargets = extraTargets,
|
||||
traceLogPacks = traceLogPacks.map(),
|
||||
writeToFiles = writesToFilesConfiguration.toTracingFileConfiguration(),
|
||||
sentryDsn = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package io.element.android.libraries.matrix.impl.widget
|
|||
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
import io.element.android.libraries.matrix.impl.room.JoinedRustRoom
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -25,7 +24,6 @@ import kotlin.coroutines.coroutineContext
|
|||
class RustWidgetDriver(
|
||||
widgetSettings: MatrixWidgetSettings,
|
||||
private val room: Room,
|
||||
private val joinedRustRoom: JoinedRustRoom,
|
||||
private val widgetCapabilitiesProvider: WidgetCapabilitiesProvider,
|
||||
) : MatrixWidgetDriver {
|
||||
// It's important to have extra capacity here to make sure we don't drop any messages
|
||||
|
|
@ -71,6 +69,5 @@ class RustWidgetDriver(
|
|||
override fun close() {
|
||||
receiveMessageJob?.cancel()
|
||||
driverAndHandle.driver.close()
|
||||
joinedRustRoom.destroy()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import org.matrix.rustcomponents.sdk.RoomHistoryVisibility
|
|||
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||
import org.matrix.rustcomponents.sdk.RoomMember
|
||||
import org.matrix.rustcomponents.sdk.RoomNotificationMode
|
||||
import org.matrix.rustcomponents.sdk.RoomTombstoneInfo
|
||||
import org.matrix.rustcomponents.sdk.SuccessorRoom
|
||||
import uniffi.matrix_sdk_base.EncryptionState
|
||||
|
||||
fun aRustRoomInfo(
|
||||
|
|
@ -30,7 +30,6 @@ fun aRustRoomInfo(
|
|||
isDirect: Boolean = false,
|
||||
isPublic: Boolean = false,
|
||||
isSpace: Boolean = false,
|
||||
tombstone: RoomTombstoneInfo? = null,
|
||||
isFavourite: Boolean = false,
|
||||
canonicalAlias: String? = null,
|
||||
alternativeAliases: List<String> = listOf(),
|
||||
|
|
@ -54,6 +53,7 @@ fun aRustRoomInfo(
|
|||
roomCreator: UserId? = null,
|
||||
joinRule: JoinRule? = null,
|
||||
historyVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Joined,
|
||||
successorRoom: SuccessorRoom? = null,
|
||||
) = RoomInfo(
|
||||
id = id,
|
||||
displayName = displayName,
|
||||
|
|
@ -64,7 +64,6 @@ fun aRustRoomInfo(
|
|||
isDirect = isDirect,
|
||||
isPublic = isPublic,
|
||||
isSpace = isSpace,
|
||||
tombstone = tombstone,
|
||||
isFavourite = isFavourite,
|
||||
canonicalAlias = canonicalAlias,
|
||||
alternativeAliases = alternativeAliases,
|
||||
|
|
@ -87,5 +86,6 @@ fun aRustRoomInfo(
|
|||
pinnedEventIds = pinnedEventIds,
|
||||
creator = roomCreator?.value,
|
||||
joinRule = joinRule,
|
||||
historyVisibility = historyVisibility
|
||||
historyVisibility = historyVisibility,
|
||||
successorRoom = successorRoom,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import org.matrix.rustcomponents.sdk.RoomDirectorySearch
|
|||
import org.matrix.rustcomponents.sdk.Session
|
||||
import org.matrix.rustcomponents.sdk.SyncServiceBuilder
|
||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||
import org.matrix.rustcomponents.sdk.UnableToDecryptDelegate
|
||||
|
||||
class FakeRustClient(
|
||||
private val userId: String = A_USER_ID.value,
|
||||
|
|
@ -34,6 +35,7 @@ class FakeRustClient(
|
|||
private val encryption: Encryption = FakeRustEncryption(),
|
||||
private val session: Session = aRustSession(),
|
||||
private val clearCachesResult: () -> Unit = { lambdaError() },
|
||||
private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() },
|
||||
private val closeResult: () -> Unit = {},
|
||||
) : Client(NoPointer) {
|
||||
override fun userId(): String = userId
|
||||
|
|
@ -58,5 +60,6 @@ class FakeRustClient(
|
|||
|
||||
override suspend fun deletePusher(identifiers: PusherIdentifiers) = Unit
|
||||
override suspend fun clearCaches() = simulateLongTask { clearCachesResult() }
|
||||
override suspend fun setUtdDelegate(utdDelegate: UnableToDecryptDelegate) = withUtdHook(utdDelegate)
|
||||
override fun close() = closeResult()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,6 @@ class FakeRustClientBuilder : ClientBuilder(NoPointer) {
|
|||
}
|
||||
|
||||
override suspend fun build(): Client {
|
||||
return FakeRustClient()
|
||||
return FakeRustClient(withUtdHook = {})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import org.matrix.rustcomponents.sdk.NoPointer
|
||||
import org.matrix.rustcomponents.sdk.NotificationClient
|
||||
import org.matrix.rustcomponents.sdk.NotificationItem
|
||||
import org.matrix.rustcomponents.sdk.NotificationItemsRequest
|
||||
|
||||
class FakeRustNotificationClient(
|
||||
var notificationItemResult: NotificationItem? = null
|
||||
var notificationItemResult: Map<String, NotificationItem> = emptyMap(),
|
||||
) : NotificationClient(NoPointer) {
|
||||
override suspend fun getNotification(roomId: String, eventId: String): NotificationItem? = simulateLongTask {
|
||||
notificationItemResult
|
||||
override suspend fun getNotifications(requests: List<NotificationItemsRequest>): Map<String, NotificationItem> {
|
||||
return notificationItemResult
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,22 @@
|
|||
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
||||
import org.matrix.rustcomponents.sdk.NoPointer
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||
import org.matrix.rustcomponents.sdk.RoomMembersIterator
|
||||
|
||||
class FakeRustRoom(
|
||||
private val roomId: RoomId = A_ROOM_ID,
|
||||
private val getMembers: () -> RoomMembersIterator = { lambdaError() },
|
||||
private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() },
|
||||
private val leaveLambda: () -> Unit = { lambdaError() },
|
||||
private val latestEventLambda: () -> EventTimelineItem? = { lambdaError() },
|
||||
private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value),
|
||||
) : Room(NoPointer) {
|
||||
override fun id(): String {
|
||||
return roomId.value
|
||||
|
|
@ -31,6 +37,18 @@ class FakeRustRoom(
|
|||
return getMembersNoSync()
|
||||
}
|
||||
|
||||
override suspend fun leave() {
|
||||
leaveLambda()
|
||||
}
|
||||
|
||||
override suspend fun roomInfo(): RoomInfo {
|
||||
return roomInfo
|
||||
}
|
||||
|
||||
override suspend fun latestEvent(): EventTimelineItem? {
|
||||
return latestEventLambda()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
// No-op
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
||||
import org.matrix.rustcomponents.sdk.NoPointer
|
||||
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
|
||||
class FakeRustRoomListItem(
|
||||
private val roomId: RoomId,
|
||||
private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value),
|
||||
private val latestEvent: EventTimelineItem? = null,
|
||||
) : RoomListItem(NoPointer) {
|
||||
override fun id(): String {
|
||||
return roomId.value
|
||||
}
|
||||
|
||||
override suspend fun roomInfo(): RoomInfo {
|
||||
return roomInfo
|
||||
}
|
||||
|
||||
override suspend fun latestEvent(): EventTimelineItem? {
|
||||
return latestEvent
|
||||
}
|
||||
}
|
||||
|
|
@ -10,10 +10,8 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes
|
|||
import org.matrix.rustcomponents.sdk.NoPointer
|
||||
import org.matrix.rustcomponents.sdk.SyncService
|
||||
import org.matrix.rustcomponents.sdk.SyncServiceBuilder
|
||||
import org.matrix.rustcomponents.sdk.UnableToDecryptDelegate
|
||||
|
||||
class FakeRustSyncServiceBuilder : SyncServiceBuilder(NoPointer) {
|
||||
override suspend fun withUtdHook(delegate: UnableToDecryptDelegate): SyncServiceBuilder = this
|
||||
override fun withOfflineMode(): SyncServiceBuilder = this
|
||||
override suspend fun finish(): SyncService = FakeRustSyncService()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustNotificat
|
|||
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_USER_ID_2
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
|
|
@ -28,12 +29,12 @@ class RustNotificationServiceTest {
|
|||
@Test
|
||||
fun test() = runTest {
|
||||
val notificationClient = FakeRustNotificationClient(
|
||||
notificationItemResult = aRustNotificationItem(),
|
||||
notificationItemResult = mapOf(AN_EVENT_ID.value to aRustNotificationItem()),
|
||||
)
|
||||
val sut = createRustNotificationService(
|
||||
notificationClient = notificationClient,
|
||||
)
|
||||
val result = sut.getNotification(A_ROOM_ID, AN_EVENT_ID).getOrThrow()!!
|
||||
val result = sut.getNotifications(mapOf(A_ROOM_ID to listOf(AN_EVENT_ID))).getOrThrow()[AN_EVENT_ID]!!
|
||||
assertThat(result.isEncrypted).isTrue()
|
||||
assertThat(result.content).isEqualTo(
|
||||
NotificationContent.MessageLike.RoomMessage(
|
||||
|
|
@ -51,6 +52,7 @@ class RustNotificationServiceTest {
|
|||
clock: SystemClock = FakeSystemClock(),
|
||||
) =
|
||||
RustNotificationService(
|
||||
sessionId = A_SESSION_ID,
|
||||
notificationClient = notificationClient,
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
clock = clock,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class RoomInfoMapperTest {
|
|||
isPublic = false,
|
||||
isSpace = false,
|
||||
joinRule = RustJoinRule.Invite,
|
||||
tombstone = null,
|
||||
successorRoom = null,
|
||||
isFavourite = false,
|
||||
canonicalAlias = A_ROOM_ALIAS.value,
|
||||
alternativeAliases = listOf(A_ROOM_ALIAS.value),
|
||||
|
|
@ -90,7 +90,6 @@ class RoomInfoMapperTest {
|
|||
isDirect = true,
|
||||
isEncrypted = true,
|
||||
isSpace = false,
|
||||
tombstone = null,
|
||||
isFavorite = false,
|
||||
joinRule = JoinRule.Invite,
|
||||
canonicalAlias = A_ROOM_ALIAS,
|
||||
|
|
@ -120,6 +119,7 @@ class RoomInfoMapperTest {
|
|||
numUnreadNotifications = 13L,
|
||||
numUnreadMentions = 14L,
|
||||
historyVisibility = RoomHistoryVisibility.Joined,
|
||||
successorRoom = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -139,7 +139,7 @@ class RoomInfoMapperTest {
|
|||
isPublic = true,
|
||||
joinRule = null,
|
||||
isSpace = false,
|
||||
tombstone = null,
|
||||
successorRoom = null,
|
||||
isFavourite = true,
|
||||
canonicalAlias = null,
|
||||
alternativeAliases = emptyList(),
|
||||
|
|
@ -175,7 +175,7 @@ class RoomInfoMapperTest {
|
|||
isDirect = false,
|
||||
joinRule = null,
|
||||
isSpace = false,
|
||||
tombstone = null,
|
||||
successorRoom = null,
|
||||
isFavorite = true,
|
||||
canonicalAlias = null,
|
||||
alternativeAliases = emptyList<RoomAlias>().toPersistentList(),
|
||||
|
|
|
|||
|
|
@ -7,15 +7,21 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import app.cash.turbine.TurbineTestContext
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
|
||||
import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -24,32 +30,121 @@ import org.junit.Test
|
|||
class RustBaseRoomTest {
|
||||
@Test
|
||||
fun `RustBaseRoom should cancel the room coroutine scope when it is destroyed`() = runTest {
|
||||
val rustBaseRoom = createRustBaseRoom(
|
||||
// Not using backgroundScope here, but the test scope
|
||||
sessionCoroutineScope = this
|
||||
)
|
||||
val rustBaseRoom = createRustBaseRoom()
|
||||
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isTrue()
|
||||
rustBaseRoom.destroy()
|
||||
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when currentUserMembership=JOINED and user leave room succeed then roomMembershipObserver emits change as LEFT`() = runTest {
|
||||
val roomMembershipObserver = RoomMembershipObserver()
|
||||
val rustBaseRoom = createRustBaseRoom(
|
||||
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.JOINED),
|
||||
innerRoom = FakeRustRoom(
|
||||
leaveLambda = {
|
||||
// Simulate a successful leave
|
||||
}
|
||||
),
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
)
|
||||
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
|
||||
val membershipUpdate = awaitItem()
|
||||
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
|
||||
assertThat(membershipUpdate.isUserInRoom).isFalse()
|
||||
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.LEFT)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when currentUserMembership=KNOCKED and user leave room succeed then roomMembershipObserver emits change as KNOCK_RETRACTED`() = runTest {
|
||||
val roomMembershipObserver = RoomMembershipObserver()
|
||||
val rustBaseRoom = createRustBaseRoom(
|
||||
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.KNOCKED),
|
||||
innerRoom = FakeRustRoom(
|
||||
leaveLambda = {
|
||||
// Simulate a successful leave
|
||||
}
|
||||
),
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
)
|
||||
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
|
||||
val membershipUpdate = awaitItem()
|
||||
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
|
||||
assertThat(membershipUpdate.isUserInRoom).isFalse()
|
||||
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.KNOCK_RETRACTED)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when currentUserMembership=INVITED and user leave room succeed then roomMembershipObserver emits change as INVITATION_REJECTED`() = runTest {
|
||||
val roomMembershipObserver = RoomMembershipObserver()
|
||||
val rustBaseRoom = createRustBaseRoom(
|
||||
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED),
|
||||
innerRoom = FakeRustRoom(
|
||||
leaveLambda = {
|
||||
// Simulate a successful leave
|
||||
}
|
||||
),
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
)
|
||||
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
|
||||
val membershipUpdate = awaitItem()
|
||||
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
|
||||
assertThat(membershipUpdate.isUserInRoom).isFalse()
|
||||
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.INVITATION_REJECTED)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when user leave room fails then roomMembershipObserver emits nothing`() = runTest {
|
||||
val roomMembershipObserver = RoomMembershipObserver()
|
||||
val rustBaseRoom = createRustBaseRoom(
|
||||
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED),
|
||||
innerRoom = FakeRustRoom(
|
||||
leaveLambda = { error("Leave failed") }
|
||||
),
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
)
|
||||
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
|
||||
// No emit
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun TestScope.leaveRoomAndObserveMembershipChange(
|
||||
roomMembershipObserver: RoomMembershipObserver,
|
||||
rustBaseRoom: RustBaseRoom,
|
||||
validate: suspend TurbineTestContext<RoomMembershipObserver.RoomMembershipUpdate>.() -> Unit
|
||||
) {
|
||||
val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1)
|
||||
rustBaseRoom.leave()
|
||||
shared.test {
|
||||
validate()
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
rustBaseRoom.destroy()
|
||||
}
|
||||
|
||||
private fun TestScope.createRustBaseRoom(
|
||||
sessionCoroutineScope: CoroutineScope,
|
||||
initialRoomInfo: RoomInfo = aRoomInfo(),
|
||||
innerRoom: FakeRustRoom = FakeRustRoom(),
|
||||
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
||||
): RustBaseRoom {
|
||||
val dispatchers = testCoroutineDispatchers()
|
||||
return RustBaseRoom(
|
||||
sessionId = A_SESSION_ID,
|
||||
deviceId = A_DEVICE_ID,
|
||||
innerRoom = FakeRustRoom(),
|
||||
innerRoom = innerRoom,
|
||||
coroutineDispatchers = dispatchers,
|
||||
roomSyncSubscriber = RoomSyncSubscriber(
|
||||
roomListService = FakeRustRoomListService(),
|
||||
dispatchers = dispatchers,
|
||||
),
|
||||
roomMembershipObserver = RoomMembershipObserver(),
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
// Not using backgroundScope here, but the test scope
|
||||
sessionCoroutineScope = this,
|
||||
roomInfoMapper = RoomInfoMapper(),
|
||||
initialRoomInfo = aRoomInfo(),
|
||||
initialRoomInfo = initialRoomInfo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
|
|||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomDescription
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomDirectorySearch
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
|
@ -31,7 +30,6 @@ class RustBaseRoomDirectoryListTest {
|
|||
val mapper = RoomDescriptionMapper()
|
||||
val sut = createRustRoomDirectoryList(
|
||||
roomDirectorySearch = roomDirectorySearch,
|
||||
scope = backgroundScope,
|
||||
)
|
||||
// Let the mxCallback be ready
|
||||
runCurrent()
|
||||
|
|
@ -81,10 +79,9 @@ class RustBaseRoomDirectoryListTest {
|
|||
|
||||
private fun TestScope.createRustRoomDirectoryList(
|
||||
roomDirectorySearch: RoomDirectorySearch = FakeRustRoomDirectorySearch(),
|
||||
scope: CoroutineScope,
|
||||
) = RustRoomDirectoryList(
|
||||
inner = roomDirectorySearch,
|
||||
coroutineScope = scope,
|
||||
coroutineScope = backgroundScope,
|
||||
coroutineContext = StandardTestDispatcher(testScheduler),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@
|
|||
package io.element.android.libraries.matrix.impl.roomlist
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListItem
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
|
|
@ -30,7 +31,7 @@ class RoomSummaryListProcessorTest {
|
|||
summaries.value = listOf(aRoomSummary())
|
||||
val processor = createProcessor()
|
||||
|
||||
val newEntry = FakeRustRoomListItem(A_ROOM_ID_2)
|
||||
val newEntry = aRustRoom(A_ROOM_ID_2)
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(newEntry, newEntry, newEntry))))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(4)
|
||||
|
|
@ -41,7 +42,7 @@ class RoomSummaryListProcessorTest {
|
|||
fun `PushBack adds a new entry at the end of the list`() = runTest {
|
||||
summaries.value = listOf(aRoomSummary())
|
||||
val processor = createProcessor()
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRustRoomListItem(A_ROOM_ID_2))))
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(aRustRoom(A_ROOM_ID_2))))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(2)
|
||||
assertThat(summaries.value.last().roomId).isEqualTo(A_ROOM_ID_2)
|
||||
|
|
@ -51,7 +52,7 @@ class RoomSummaryListProcessorTest {
|
|||
fun `PushFront inserts a new entry at the start of the list`() = runTest {
|
||||
summaries.value = listOf(aRoomSummary())
|
||||
val processor = createProcessor()
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRustRoomListItem(A_ROOM_ID_2))))
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(aRustRoom(A_ROOM_ID_2))))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(2)
|
||||
assertThat(summaries.value.first().roomId).isEqualTo(A_ROOM_ID_2)
|
||||
|
|
@ -63,7 +64,7 @@ class RoomSummaryListProcessorTest {
|
|||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), FakeRustRoomListItem(A_ROOM_ID_2))))
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), aRustRoom(A_ROOM_ID_2))))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(1)
|
||||
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2)
|
||||
|
|
@ -75,7 +76,7 @@ class RoomSummaryListProcessorTest {
|
|||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), FakeRustRoomListItem(A_ROOM_ID_2))))
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), aRustRoom(A_ROOM_ID_2))))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(2)
|
||||
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2)
|
||||
|
|
@ -163,12 +164,17 @@ class RoomSummaryListProcessorTest {
|
|||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Reset(listOf(FakeRustRoomListItem(A_ROOM_ID_3)))))
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Reset(listOf(aRustRoom(A_ROOM_ID_3)))))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(1)
|
||||
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_3)
|
||||
}
|
||||
|
||||
private fun aRustRoom(roomId: RoomId = A_ROOM_ID) = FakeRustRoom(
|
||||
roomId = roomId,
|
||||
latestEventLambda = { null },
|
||||
)
|
||||
|
||||
private fun TestScope.createProcessor() = RoomSummaryListProcessor(
|
||||
summaries,
|
||||
FakeRustRoomListService(),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
|||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
|
||||
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
|
@ -28,7 +27,6 @@ class RustBaseRoomListServiceTest {
|
|||
fun `syncIndicator should emit the expected values`() = runTest {
|
||||
val roomListService = FakeRustRoomListService()
|
||||
val sut = createRustRoomListService(
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
roomListService = roomListService,
|
||||
)
|
||||
// Give time for mxCallback to setup
|
||||
|
|
@ -44,18 +42,17 @@ class RustBaseRoomListServiceTest {
|
|||
}
|
||||
|
||||
private fun TestScope.createRustRoomListService(
|
||||
sessionCoroutineScope: CoroutineScope,
|
||||
roomListService: RustRoomListService = FakeRustRoomListService(),
|
||||
) = RustRoomListService(
|
||||
innerRoomListService = roomListService,
|
||||
sessionDispatcher = StandardTestDispatcher(testScheduler),
|
||||
roomListFactory = RoomListFactory(
|
||||
innerRoomListService = roomListService,
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
),
|
||||
roomSyncSubscriber = RoomSyncSubscriber(
|
||||
roomListService = roomListService,
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
),
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineI
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -38,7 +37,6 @@ class TimelineItemsSubscriberTest {
|
|||
MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
|
||||
val timeline = FakeRustTimeline()
|
||||
val timelineItemsSubscriber = createTimelineItemsSubscriber(
|
||||
coroutineScope = backgroundScope,
|
||||
timeline = timeline,
|
||||
timelineItems = timelineItems,
|
||||
)
|
||||
|
|
@ -59,7 +57,6 @@ class TimelineItemsSubscriberTest {
|
|||
MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
|
||||
val timeline = FakeRustTimeline()
|
||||
val timelineItemsSubscriber = createTimelineItemsSubscriber(
|
||||
coroutineScope = backgroundScope,
|
||||
timeline = timeline,
|
||||
timelineItems = timelineItems,
|
||||
)
|
||||
|
|
@ -81,7 +78,6 @@ class TimelineItemsSubscriberTest {
|
|||
val timeline = FakeRustTimeline()
|
||||
val onNewSyncedEventRecorder = lambdaRecorder<Unit> { }
|
||||
val timelineItemsSubscriber = createTimelineItemsSubscriber(
|
||||
coroutineScope = backgroundScope,
|
||||
timeline = timeline,
|
||||
timelineItems = timelineItems,
|
||||
onNewSyncedEvent = onNewSyncedEventRecorder,
|
||||
|
|
@ -109,9 +105,7 @@ class TimelineItemsSubscriberTest {
|
|||
|
||||
@Test
|
||||
fun `multiple subscriptions does not have side effect`() = runTest {
|
||||
val timelineItemsSubscriber = createTimelineItemsSubscriber(
|
||||
coroutineScope = backgroundScope,
|
||||
)
|
||||
val timelineItemsSubscriber = createTimelineItemsSubscriber()
|
||||
timelineItemsSubscriber.subscribeIfNeeded()
|
||||
timelineItemsSubscriber.subscribeIfNeeded()
|
||||
timelineItemsSubscriber.unsubscribeIfNeeded()
|
||||
|
|
@ -120,7 +114,6 @@ class TimelineItemsSubscriberTest {
|
|||
}
|
||||
|
||||
private fun TestScope.createTimelineItemsSubscriber(
|
||||
coroutineScope: CoroutineScope,
|
||||
timeline: Timeline = FakeRustTimeline(),
|
||||
timelineItems: MutableSharedFlow<List<MatrixTimelineItem>> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE),
|
||||
initLatch: CompletableDeferred<Unit> = CompletableDeferred(),
|
||||
|
|
@ -128,7 +121,7 @@ private fun TestScope.createTimelineItemsSubscriber(
|
|||
onNewSyncedEvent: () -> Unit = { lambdaError() },
|
||||
): TimelineItemsSubscriber {
|
||||
return TimelineItemsSubscriber(
|
||||
timelineCoroutineScope = coroutineScope,
|
||||
timelineCoroutineScope = backgroundScope,
|
||||
dispatcher = StandardTestDispatcher(testScheduler),
|
||||
timeline = timeline,
|
||||
timelineDiffProcessor = createMatrixTimelineDiffProcessor(timelineItems),
|
||||
|
|
|
|||
|
|
@ -69,6 +69,10 @@ const val A_REDACTION_REASON = "A redaction reason"
|
|||
const val A_HOMESERVER_URL = "matrix.org"
|
||||
const val A_HOMESERVER_URL_2 = "matrix-client.org"
|
||||
|
||||
const val AN_ACCOUNT_PROVIDER = "matrix.org"
|
||||
const val AN_ACCOUNT_PROVIDER_2 = "element.io"
|
||||
const val AN_ACCOUNT_PROVIDER_3 = "other.io"
|
||||
|
||||
val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false)
|
||||
val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
|
||||
val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE
|
||||
|
|
|
|||
|
|
@ -13,16 +13,13 @@ import io.element.android.libraries.matrix.api.notification.NotificationData
|
|||
import io.element.android.libraries.matrix.api.notification.NotificationService
|
||||
|
||||
class FakeNotificationService : NotificationService {
|
||||
private var getNotificationResult: Result<NotificationData?> = Result.success(null)
|
||||
private var getNotificationsResult: Result<Map<EventId, NotificationData>> = Result.success(emptyMap())
|
||||
|
||||
fun givenGetNotificationResult(result: Result<NotificationData?>) {
|
||||
getNotificationResult = result
|
||||
fun givenGetNotificationsResult(result: Result<Map<EventId, NotificationData>>) {
|
||||
getNotificationsResult = result
|
||||
}
|
||||
|
||||
override suspend fun getNotification(
|
||||
roomId: RoomId,
|
||||
eventId: EventId,
|
||||
): Result<NotificationData?> {
|
||||
return getNotificationResult
|
||||
override suspend fun getNotifications(ids: Map<RoomId, List<EventId>>): Result<Map<EventId, NotificationData>> {
|
||||
return getNotificationsResult
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_TIMESTAMP
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME_2
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ fun aNotificationData(
|
|||
roomDisplayName: String? = A_ROOM_NAME
|
||||
): NotificationData {
|
||||
return NotificationData(
|
||||
sessionId = A_SESSION_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
threadId = threadId,
|
||||
roomId = A_ROOM_ID,
|
||||
|
|
|
|||
|
|
@ -93,7 +93,15 @@ class FakeBaseRoom(
|
|||
return powerLevelsResult()
|
||||
}
|
||||
|
||||
override fun destroy() = Unit
|
||||
private var isDestroyed = false
|
||||
|
||||
override fun destroy() {
|
||||
isDestroyed = true
|
||||
}
|
||||
|
||||
fun assertDestroyed() {
|
||||
check(isDestroyed) { "Room should be destroyed" }
|
||||
}
|
||||
|
||||
override suspend fun userDisplayName(userId: UserId): Result<String?> = simulateLongTask {
|
||||
userDisplayNameResult(userId)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
|||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.RoomTombstone
|
||||
import io.element.android.libraries.matrix.api.room.SuccessorRoom
|
||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
|
@ -39,7 +39,7 @@ fun aRoomInfo(
|
|||
isEncrypted: Boolean = false,
|
||||
joinRule: JoinRule? = JoinRule.Public,
|
||||
isSpace: Boolean = false,
|
||||
tombstone: RoomTombstone? = null,
|
||||
successorRoom: SuccessorRoom? = null,
|
||||
isFavorite: Boolean = false,
|
||||
canonicalAlias: RoomAlias? = null,
|
||||
alternativeAliases: List<RoomAlias> = emptyList(),
|
||||
|
|
@ -73,7 +73,7 @@ fun aRoomInfo(
|
|||
isEncrypted = isEncrypted,
|
||||
joinRule = joinRule,
|
||||
isSpace = isSpace,
|
||||
tombstone = tombstone,
|
||||
successorRoom = successorRoom,
|
||||
isFavorite = isFavorite,
|
||||
canonicalAlias = canonicalAlias,
|
||||
alternativeAliases = alternativeAliases.toImmutableList(),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
|||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.RoomTombstone
|
||||
import io.element.android.libraries.matrix.api.room.SuccessorRoom
|
||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
|
|
@ -52,7 +52,7 @@ fun aRoomSummary(
|
|||
isEncrypted: Boolean = false,
|
||||
joinRule: JoinRule? = JoinRule.Public,
|
||||
isSpace: Boolean = false,
|
||||
tombstone: RoomTombstone? = null,
|
||||
successorRoom: SuccessorRoom? = null,
|
||||
isFavorite: Boolean = false,
|
||||
canonicalAlias: RoomAlias? = null,
|
||||
alternativeAliases: List<RoomAlias> = emptyList(),
|
||||
|
|
@ -88,7 +88,7 @@ fun aRoomSummary(
|
|||
isEncrypted = isEncrypted,
|
||||
joinRule = joinRule,
|
||||
isSpace = isSpace,
|
||||
tombstone = tombstone,
|
||||
successorRoom = successorRoom,
|
||||
isFavorite = isFavorite,
|
||||
canonicalAlias = canonicalAlias,
|
||||
alternativeAliases = alternativeAliases.toPersistentList(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Enviar convite"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_message">"Gostaria de iniciar um bate-papo com %1$s?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_title">"Enviar convite?"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s(%2$s) convidou você"</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="screen_invites_invited_you">"%1$s (%2$s) نے آپ کو مدعو کیا"</string>
|
||||
</resources>
|
||||
|
|
@ -37,7 +37,24 @@ class MediaSenderTest {
|
|||
@Test
|
||||
fun `given an attachment when sending it the preprocessor always runs`() = runTest {
|
||||
val preProcessor = FakeMediaPreProcessor()
|
||||
val sender = createMediaSender(preProcessor)
|
||||
val sender = createMediaSender(
|
||||
preProcessor = preProcessor,
|
||||
room = FakeJoinedRoom(
|
||||
liveTimeline = FakeTimeline().apply {
|
||||
sendFileLambda = lambdaRecorder<
|
||||
File,
|
||||
FileInfo,
|
||||
String?,
|
||||
String?,
|
||||
ProgressCallback?,
|
||||
ReplyParameters?,
|
||||
Result<FakeMediaUploadHandler>,
|
||||
> { _, _, _, _, _, _ ->
|
||||
Result.success(FakeMediaUploadHandler())
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_media_browser_delete_confirmation_subtitle">"Fitxategia gelatik kenduko da eta kideek ezingo dute atzitu."</string>
|
||||
<string name="screen_media_browser_delete_confirmation_title">"Fitxategia ezabatu?"</string>
|
||||
<string name="screen_media_browser_download_error_message">"Egiaztatu Interneteko konexioa eta saiatu berriro."</string>
|
||||
<string name="screen_media_browser_files_empty_state_subtitle">"Gela honetara igotako dokumentuak, audio-fitxategiak, eta ahots-mezuak hemen erakutsiko dira."</string>
|
||||
<string name="screen_media_browser_files_empty_state_title">"Oraindik ez da fitxategirik igo"</string>
|
||||
<string name="screen_media_browser_list_loading_files">"Fitxategiak kargatzen…"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_media_browser_delete_confirmation_subtitle">"Esse arquivo será removido da sala e os membros não terão acesso a ele."</string>
|
||||
<string name="screen_media_browser_delete_confirmation_title">"Excluir arquivo?"</string>
|
||||
<string name="screen_media_browser_download_error_message">"Verifique sua conexão com a Internet e tente novamente."</string>
|
||||
<string name="screen_media_browser_files_empty_state_subtitle">"Os documentos, arquivos de áudio e mensagens de voz enviados para esta sala serão exibidos aqui."</string>
|
||||
<string name="screen_media_browser_files_empty_state_title">"Ainda não foram enviados arquivos"</string>
|
||||
<string name="screen_media_browser_list_loading_files">"Carregando arquivos…"</string>
|
||||
<string name="screen_media_browser_list_loading_media">"Carregando mídia…"</string>
|
||||
<string name="screen_media_browser_list_mode_files">"Arquivos"</string>
|
||||
<string name="screen_media_browser_list_mode_media">"Mídia"</string>
|
||||
<string name="screen_media_browser_media_empty_state_subtitle">"As imagens e os vídeos enviados para esta sala serão exibidos aqui."</string>
|
||||
<string name="screen_media_browser_media_empty_state_title">"Nenhuma mídia foi carregada ainda"</string>
|
||||
<string name="screen_media_browser_title">"Mídia e arquivos"</string>
|
||||
<string name="screen_media_details_file_format">"Formato de arquivo"</string>
|
||||
<string name="screen_media_details_filename">"Nome do arquivo"</string>
|
||||
<string name="screen_media_details_no_more_files_to_show">"Não há mais arquivos para mostrar"</string>
|
||||
<string name="screen_media_details_no_more_media_to_show">"Não há mais mídia para mostrar"</string>
|
||||
<string name="screen_media_details_uploaded_by">"Enviado por"</string>
|
||||
<string name="screen_media_details_uploaded_on">"Enviado em"</string>
|
||||
</resources>
|
||||
|
|
@ -40,6 +40,7 @@ dependencies {
|
|||
api(projects.libraries.oidc.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.oidc.impl
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DefaultOidcIntentResolverTest {
|
||||
@Test
|
||||
fun `test resolve oidc go back`() {
|
||||
val sut = createDefaultOidcIntentResolver()
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = "io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO".toUri()
|
||||
}
|
||||
val result = sut.resolve(intent)
|
||||
assertThat(result).isEqualTo(OidcAction.GoBack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test resolve oidc success`() {
|
||||
val sut = createDefaultOidcIntentResolver()
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB".toUri()
|
||||
}
|
||||
val result = sut.resolve(intent)
|
||||
assertThat(result).isEqualTo(
|
||||
OidcAction.Success(
|
||||
url = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test resolve oidc invalid`() {
|
||||
val sut = createDefaultOidcIntentResolver()
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = "io.element.android:/invalid".toUri()
|
||||
}
|
||||
assertThrows(IllegalStateException::class.java) {
|
||||
sut.resolve(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDefaultOidcIntentResolver(): DefaultOidcIntentResolver {
|
||||
return DefaultOidcIntentResolver(
|
||||
oidcUrlParser = DefaultOidcUrlParser(
|
||||
oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.oidc.impl.customtab
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultOidcActionFlowTest {
|
||||
@Test
|
||||
fun `collect gets all the posted events`() = runTest {
|
||||
val data = mutableListOf<OidcAction?>()
|
||||
val sut = DefaultOidcActionFlow()
|
||||
backgroundScope.launch {
|
||||
sut.collect { action ->
|
||||
data.add(action)
|
||||
}
|
||||
}
|
||||
sut.post(OidcAction.GoBack)
|
||||
delay(1)
|
||||
sut.reset()
|
||||
delay(1)
|
||||
assertThat(data).containsExactly(OidcAction.GoBack, null)
|
||||
}
|
||||
}
|
||||
20
libraries/oidc/test/build.gradle.kts
Normal file
20
libraries/oidc/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.oidc.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.coroutines.core)
|
||||
api(projects.libraries.oidc.api)
|
||||
implementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.oidc.test
|
||||
|
||||
import android.content.Intent
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcIntentResolver
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeOidcIntentResolver(
|
||||
private val resolveResult: (Intent) -> OidcAction? = { lambdaError() }
|
||||
) : OidcIntentResolver {
|
||||
override fun resolve(intent: Intent): OidcAction? {
|
||||
return resolveResult(intent)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.oidc.test.customtab
|
||||
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/**
|
||||
* This is actually a copy of DefaultOidcActionFlow.
|
||||
*/
|
||||
class FakeOidcActionFlow : OidcActionFlow {
|
||||
private val mutableStateFlow = MutableStateFlow<OidcAction?>(null)
|
||||
|
||||
override fun post(oidcAction: OidcAction) {
|
||||
mutableStateFlow.value = oidcAction
|
||||
}
|
||||
|
||||
override suspend fun collect(collector: FlowCollector<OidcAction?>) {
|
||||
mutableStateFlow.collect(collector)
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
mutableStateFlow.value = null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +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">"Damit die Anwendung die Kamera verwenden kann, erteile bitte die Erlaubnis in den Systemeinstellungen."</string>
|
||||
<string name="dialog_permission_camera">"Damit die Anwendung die Kamera verwenden kann, erteilen Sie bitte die Erlaubnis in den Systemeinstellungen."</string>
|
||||
<string name="dialog_permission_generic">"Bitte erteile die Erlaubnis in den Systemeinstellungen."</string>
|
||||
<string name="dialog_permission_microphone">"Damit die Anwendung das Mikrofon verwenden kann, erteile bitte die Erlaubnis in den Systemeinstellungen."</string>
|
||||
<string name="dialog_permission_notification">"Damit die Anwendung Benachrichtigungen anzeigen kann, erteile bitte die Erlaubnis in den Systemeinstellungen."</string>
|
||||
<string name="dialog_permission_microphone">"Damit die Anwendung das Mikrofon verwenden kann, erteilen Sie bitte die Erlaubnis in den Systemeinstellungen."</string>
|
||||
<string name="dialog_permission_notification">"Damit Benachrichtigungen angezeigt werden können, gewähren Sie der App bitte die Erlaubnis in den Systemeinstellungen."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="dialog_permission_camera">"اطلاقیے کو تصویرگر استعمال کرنے دینے کے لیے، برائے مہربانی نظام کی ترتیبات میں اجازت دیں۔"</string>
|
||||
<string name="dialog_permission_generic">"برائے مہربانی نظام کی ترتیبات میں اجازت دیں۔"</string>
|
||||
<string name="dialog_permission_microphone">"اطلاقیے کو صوتگر استعمال کرنے دینے کے لیے، برائے مہربانی نظام کی ترتیبات میں اجازت دیں۔"</string>
|
||||
<string name="dialog_permission_notification">"اطلاقیے کو اطلاعات ظاہر کرنے دینے کے لیے، برائے مہربانی نظام کی ترتیبات میں اجازت دیں۔"</string>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<?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_description">"Stelle sicher, dass die Anwendung Benachrichtigungen anzeigen kann."</string>
|
||||
<string name="troubleshoot_notifications_test_check_permission_description">"Prüfe, dass die Anwendung Benachrichtigungen anzeigen kann."</string>
|
||||
<string name="troubleshoot_notifications_test_check_permission_title">"Berechtigungen überprüfen"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<?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_description">"Verifique se o aplicativo pode mostrar notificações."</string>
|
||||
<string name="troubleshoot_notifications_test_check_permission_title">"Verifique as permissões"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?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_description">"پڑتال کریں کہ اطلاقیہ اطلاعات دکھا سکتا ہے"</string>
|
||||
<string name="troubleshoot_notifications_test_check_permission_title">"اجازتوں کی پڑتال کریں"</string>
|
||||
</resources>
|
||||
|
|
@ -10,8 +10,8 @@ package io.element.android.libraries.permissions.impl.troubleshoot
|
|||
import android.os.Build
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
||||
import io.element.android.libraries.permissions.impl.action.FakePermissionActions
|
||||
import io.element.android.libraries.permissions.test.FakePermissionStateProvider
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
|
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.permissions.impl
|
||||
package io.element.android.libraries.permissions.test
|
||||
|
||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -11,10 +11,10 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
|
@ -61,10 +61,14 @@ private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.No
|
|||
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
|
||||
*/
|
||||
interface NotifiableEventResolver {
|
||||
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent>
|
||||
suspend fun resolveEvents(
|
||||
sessionId: SessionId,
|
||||
notificationEventRequests: List<NotificationEventRequest>
|
||||
): Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class DefaultNotifiableEventResolver @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val clock: SystemClock,
|
||||
|
|
@ -75,29 +79,34 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
private val callNotificationEventResolver: CallNotificationEventResolver,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : NotifiableEventResolver {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent> {
|
||||
// Restore session
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return Result.failure(
|
||||
ResolvingException("Unable to restore session for $sessionId")
|
||||
)
|
||||
val notificationService = client.notificationService()
|
||||
val notificationData = notificationService.getNotification(
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
).onFailure {
|
||||
Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.")
|
||||
override suspend fun resolveEvents(
|
||||
sessionId: SessionId,
|
||||
notificationEventRequests: List<NotificationEventRequest>
|
||||
): Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>> {
|
||||
Timber.d("Queueing notifications: $notificationEventRequests")
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrElse {
|
||||
return Result.failure(IllegalStateException("Couldn't get or restore client for session $sessionId"))
|
||||
}
|
||||
val ids = notificationEventRequests.groupBy { it.roomId }.mapValues { (_, value) -> value.map { it.eventId } }
|
||||
|
||||
// TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event
|
||||
return notificationData.flatMap {
|
||||
if (it == null) {
|
||||
Timber.tag(loggerTag.value).d("No notification data found for event $eventId")
|
||||
return@flatMap Result.failure(ResolvingException("Unable to resolve event $eventId"))
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("Found notification item for $eventId")
|
||||
it.asNotifiableEvent(client, sessionId)
|
||||
val notifications = client.notificationService().getNotifications(ids).mapCatching { map ->
|
||||
map.mapValues { (_, notificationData) ->
|
||||
notificationData.asNotifiableEvent(client, sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success(
|
||||
notificationEventRequests.associate {
|
||||
val notificationData = notifications.getOrNull()?.get(it.eventId)
|
||||
if (notificationData != null) {
|
||||
it to notificationData
|
||||
} else {
|
||||
// TODO once the SDK can actually return what went wrong, we should return it here instead of this generic error
|
||||
it to Result.failure(ResolvingException("No notification data for ${it.roomId} - ${it.eventId}"))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun NotificationData.asNotifiableEvent(
|
||||
|
|
|
|||
|
|
@ -113,6 +113,11 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
renderEvents(listOf(notifiableEvent))
|
||||
}
|
||||
|
||||
suspend fun onNotifiableEventsReceived(notifiableEvents: List<NotifiableEvent>) {
|
||||
val eventsToNotify = notifiableEvents.filter { !it.shouldIgnoreEventInRoom(appNavigationStateService.appNavigationState.value) }
|
||||
renderEvents(eventsToNotify)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all known message events for a [sessionId].
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ class DefaultOnMissedCallNotificationHandler @Inject constructor(
|
|||
// 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(roomId, eventId)
|
||||
?.getNotifications(mapOf(roomId to listOf(eventId)))
|
||||
?.getOrNull()
|
||||
?.get(eventId)
|
||||
?: return
|
||||
|
||||
val notifiableEvent = callNotificationEventResolver.resolveEvent(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
|||
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.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -44,6 +45,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
|||
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
||||
private val stringProvider: StringProvider,
|
||||
private val replyMessageExtractor: ReplyMessageExtractor,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
) {
|
||||
fun onReceive(intent: Intent) {
|
||||
val sessionId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return
|
||||
|
|
@ -117,13 +119,15 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
|
||||
client.getJoinedRoom(roomId)?.let { room ->
|
||||
val room = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId) ?: client.getJoinedRoom(roomId)
|
||||
|
||||
room?.let {
|
||||
sendMatrixEvent(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
replyToEventId = replyToEventId,
|
||||
threadId = threadId,
|
||||
room = room,
|
||||
room = it,
|
||||
message = message,
|
||||
)
|
||||
}
|
||||
|
|
@ -159,7 +163,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
|||
roomIsDm = room.isDm(),
|
||||
outGoingMessage = true,
|
||||
)
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent)
|
||||
onNotifiableEventReceived.onNotifiableEventsReceived(listOf(notifiableMessageEvent))
|
||||
|
||||
if (threadId != null && replyToEventId != null) {
|
||||
room.liveTimeline.replyMessage(
|
||||
|
|
@ -177,9 +181,11 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
|||
)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed to send smart reply message")
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(
|
||||
notifiableMessageEvent.copy(
|
||||
outGoingMessageFailed = true
|
||||
onNotifiableEventReceived.onNotifiableEventsReceived(
|
||||
listOf(
|
||||
notifiableMessageEvent.copy(
|
||||
outGoingMessageFailed = true
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ class DefaultNotificationDisplayer @Inject constructor(
|
|||
return false
|
||||
}
|
||||
notificationManager.notify(tag, id, notification)
|
||||
Timber.d("Notifying with tag: $tag, id: $id")
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
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.impl.notifications.model.ResolvedPushEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* This class is responsible for periodically batching notification requests and resolving them in a single call,
|
||||
* so that we can avoid having to resolve each notification individually in the SDK.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class NotificationResolverQueue @Inject constructor(
|
||||
private val notifiableEventResolver: NotifiableEventResolver,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
) {
|
||||
companion object {
|
||||
private const val BATCH_WINDOW_MS = 250L
|
||||
}
|
||||
private val requestQueue = Channel<NotificationEventRequest>(capacity = 100)
|
||||
|
||||
private var currentProcessingJob: Job? = null
|
||||
|
||||
/**
|
||||
* A flow that emits pairs of a list of notification event requests and a map of the resolved events.
|
||||
* The map contains the original request as the key and the resolved event as the value.
|
||||
*/
|
||||
val results: SharedFlow<Pair<List<NotificationEventRequest>, Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> = MutableSharedFlow()
|
||||
|
||||
/**
|
||||
* Enqueues a notification event request to be resolved.
|
||||
* The request will be processed in batches, so it may not be resolved immediately.
|
||||
*
|
||||
* @param request The notification event request to enqueue.
|
||||
*/
|
||||
suspend fun enqueue(request: NotificationEventRequest) {
|
||||
// Cancel previous processing job if it exists, acting as a debounce operation
|
||||
Timber.d("Cancelling job: $currentProcessingJob")
|
||||
currentProcessingJob?.cancel()
|
||||
|
||||
// Enqueue the request and start a delayed processing job
|
||||
requestQueue.send(request)
|
||||
currentProcessingJob = processQueue()
|
||||
Timber.d("Starting processing job for request: $request")
|
||||
}
|
||||
|
||||
private fun processQueue() = appCoroutineScope.launch(SupervisorJob()) {
|
||||
delay(BATCH_WINDOW_MS.milliseconds)
|
||||
|
||||
// If this job is still active (so this is the latest job), we launch a separate one that won't be cancelled when enqueueing new items
|
||||
// to process the existing queued items.
|
||||
appCoroutineScope.launch {
|
||||
val groupedRequestsById = buildList {
|
||||
while (!requestQueue.isEmpty) {
|
||||
requestQueue.receiveCatching().getOrNull()?.let(this::add)
|
||||
}
|
||||
}.groupBy { it.sessionId }
|
||||
|
||||
val sessionIds = groupedRequestsById.keys
|
||||
for (sessionId in sessionIds) {
|
||||
val requests = groupedRequestsById[sessionId].orEmpty()
|
||||
Timber.d("Fetching notifications for $sessionId: $requests. Pending requests: ${!requestQueue.isEmpty}")
|
||||
// Resolving the events in parallel should improve performance since each session id will query a different Client
|
||||
launch {
|
||||
// No need for a Mutex since the SDK already has one internally
|
||||
val notifications = notifiableEventResolver.resolveEvents(sessionId, requests).getOrNull().orEmpty()
|
||||
(results as MutableSharedFlow).emit(requests to notifications)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class NotificationEventRequest(
|
||||
val sessionId: SessionId,
|
||||
val roomId: RoomId,
|
||||
val eventId: EventId,
|
||||
val providerInfo: String,
|
||||
)
|
||||
|
|
@ -12,12 +12,22 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
sealed interface ResolvedPushEvent {
|
||||
data class Event(val notifiableEvent: NotifiableEvent) : ResolvedPushEvent
|
||||
val sessionId: SessionId
|
||||
val roomId: RoomId
|
||||
val eventId: EventId
|
||||
|
||||
data class Event(val notifiableEvent: NotifiableEvent) : ResolvedPushEvent {
|
||||
override val sessionId: SessionId = notifiableEvent.sessionId
|
||||
override val roomId: RoomId = notifiableEvent.roomId
|
||||
override val eventId: EventId = notifiableEvent.eventId
|
||||
}
|
||||
|
||||
data class Redaction(
|
||||
val sessionId: SessionId,
|
||||
val roomId: RoomId,
|
||||
override val sessionId: SessionId,
|
||||
override val roomId: RoomId,
|
||||
val redactedEventId: EventId,
|
||||
val reason: String?,
|
||||
) : ResolvedPushEvent
|
||||
) : ResolvedPushEvent {
|
||||
override val eventId: EventId = redactedEventId
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.push.impl.history.PushHistoryService
|
||||
import io.element.android.libraries.push.impl.history.onDiagnosticPush
|
||||
|
|
@ -20,8 +21,10 @@ import io.element.android.libraries.push.impl.history.onInvalidPushReceived
|
|||
import io.element.android.libraries.push.impl.history.onSuccess
|
||||
import io.element.android.libraries.push.impl.history.onUnableToResolveEvent
|
||||
import io.element.android.libraries.push.impl.history.onUnableToRetrieveSession
|
||||
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationEventRequest
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationResolverQueue
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
||||
|
|
@ -30,17 +33,21 @@ import io.element.android.libraries.pushproviders.api.PushData
|
|||
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag)
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushHandler @Inject constructor(
|
||||
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
||||
private val onRedactedEventReceived: OnRedactedEventReceived,
|
||||
private val notifiableEventResolver: NotifiableEventResolver,
|
||||
private val incrementPushDataStore: IncrementPushDataStore,
|
||||
private val userPushStoreFactory: UserPushStoreFactory,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
|
|
@ -50,7 +57,85 @@ class DefaultPushHandler @Inject constructor(
|
|||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||
private val notificationChannels: NotificationChannels,
|
||||
private val pushHistoryService: PushHistoryService,
|
||||
private val resolverQueue: NotificationResolverQueue,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
) : PushHandler {
|
||||
init {
|
||||
processPushEventResults()
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the push notification event results emitted by the [resolverQueue].
|
||||
*/
|
||||
private fun processPushEventResults() {
|
||||
resolverQueue.results
|
||||
.map { (requests, resolvedEvents) ->
|
||||
for (request in requests) {
|
||||
// Log the result of the push notification event
|
||||
val result = resolvedEvents[request]
|
||||
if (result?.isSuccess == true) {
|
||||
pushHistoryService.onSuccess(
|
||||
providerInfo = request.providerInfo,
|
||||
eventId = request.eventId,
|
||||
roomId = request.roomId,
|
||||
sessionId = request.sessionId,
|
||||
comment = "Push handled successfully",
|
||||
)
|
||||
} else {
|
||||
pushHistoryService.onUnableToResolveEvent(
|
||||
providerInfo = request.providerInfo,
|
||||
eventId = request.eventId,
|
||||
roomId = request.roomId,
|
||||
sessionId = request.sessionId,
|
||||
reason = "Push not handled",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val events = mutableListOf<NotifiableEvent>()
|
||||
val redactions = mutableListOf<ResolvedPushEvent.Redaction>()
|
||||
|
||||
@Suppress("LoopWithTooManyJumpStatements")
|
||||
for (result in resolvedEvents.values) {
|
||||
val event = result.getOrNull() ?: continue
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(event.sessionId)
|
||||
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
||||
// If notifications are disabled for this session and device, we don't want to show the notification
|
||||
// But if it's a ringing call, we want to show it anyway
|
||||
val isRingingCall = (event as? ResolvedPushEvent.Event)?.notifiableEvent is NotifiableRingingCallEvent
|
||||
if (!areNotificationsEnabled && !isRingingCall) continue
|
||||
|
||||
// We categorise each result into either a NotifiableEvent or a Redaction
|
||||
when (event) {
|
||||
is ResolvedPushEvent.Event -> {
|
||||
events.add(event.notifiableEvent)
|
||||
}
|
||||
is ResolvedPushEvent.Redaction -> {
|
||||
redactions.add(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process redactions of messages
|
||||
if (redactions.isNotEmpty()) {
|
||||
onRedactedEventReceived.onRedactedEventsReceived(redactions)
|
||||
}
|
||||
|
||||
// Find and process ringing call notifications separately
|
||||
val (ringingCallEvents, nonRingingCallEvents) = events.partition { it is NotifiableRingingCallEvent }
|
||||
for (ringingCallEvent in ringingCallEvents) {
|
||||
Timber.tag(loggerTag.value).d("Ringing call event: $ringingCallEvent")
|
||||
handleRingingCallEvent(ringingCallEvent as NotifiableRingingCallEvent)
|
||||
}
|
||||
|
||||
// Finally, process other notifications (messages, invites, generic notifications, etc.)
|
||||
if (nonRingingCallEvents.isNotEmpty()) {
|
||||
onNotifiableEventReceived.onNotifiableEventsReceived(nonRingingCallEvents)
|
||||
}
|
||||
}
|
||||
.launchIn(appCoroutineScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when message is received.
|
||||
*
|
||||
|
|
@ -119,52 +204,17 @@ class DefaultPushHandler @Inject constructor(
|
|||
)
|
||||
return
|
||||
}
|
||||
notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId).fold(
|
||||
onSuccess = { resolvedPushEvent ->
|
||||
pushHistoryService.onSuccess(
|
||||
providerInfo = providerInfo,
|
||||
eventId = pushData.eventId,
|
||||
roomId = pushData.roomId,
|
||||
sessionId = userId,
|
||||
comment = resolvedPushEvent.javaClass.simpleName,
|
||||
)
|
||||
|
||||
when (resolvedPushEvent) {
|
||||
is ResolvedPushEvent.Event -> {
|
||||
when (val notifiableEvent = resolvedPushEvent.notifiableEvent) {
|
||||
is NotifiableRingingCallEvent -> {
|
||||
Timber.tag(loggerTag.value).d("Notifiable event ${pushData.eventId} is ringing call: $notifiableEvent")
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
handleRingingCallEvent(notifiableEvent)
|
||||
}
|
||||
else -> {
|
||||
Timber.tag(loggerTag.value).d("Notifiable event ${pushData.eventId} is normal event: $notifiableEvent")
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(userId)
|
||||
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
||||
if (areNotificationsEnabled) {
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is ResolvedPushEvent.Redaction -> {
|
||||
onRedactedEventReceived.onRedactedEventReceived(resolvedPushEvent)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { failure ->
|
||||
Timber.tag(loggerTag.value).w(failure, "Unable to get a notification data")
|
||||
pushHistoryService.onUnableToResolveEvent(
|
||||
providerInfo = providerInfo,
|
||||
eventId = pushData.eventId,
|
||||
roomId = pushData.roomId,
|
||||
sessionId = userId,
|
||||
reason = failure.message ?: failure.javaClass.simpleName,
|
||||
)
|
||||
}
|
||||
)
|
||||
appCoroutineScope.launch {
|
||||
val notificationEventRequest = NotificationEventRequest(
|
||||
sessionId = userId,
|
||||
roomId = pushData.roomId,
|
||||
eventId = pushData.eventId,
|
||||
providerInfo = providerInfo,
|
||||
)
|
||||
Timber.d("Queueing notification: $notificationEventRequest")
|
||||
resolverQueue.enqueue(notificationEventRequest)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import kotlinx.coroutines.launch
|
|||
import javax.inject.Inject
|
||||
|
||||
interface OnNotifiableEventReceived {
|
||||
fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent)
|
||||
fun onNotifiableEventsReceived(notifiableEvents: List<NotifiableEvent>)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
|
|
@ -26,12 +26,10 @@ class DefaultOnNotifiableEventReceived @Inject constructor(
|
|||
private val coroutineScope: CoroutineScope,
|
||||
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
|
||||
) : OnNotifiableEventReceived {
|
||||
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
||||
override fun onNotifiableEventsReceived(notifiableEvents: List<NotifiableEvent>) {
|
||||
coroutineScope.launch {
|
||||
launch { syncOnNotifiableEvent(notifiableEvent) }
|
||||
if (notifiableEvent !is NotifiableRingingCallEvent) {
|
||||
defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
|
||||
}
|
||||
launch { syncOnNotifiableEvent(notifiableEvents) }
|
||||
defaultNotificationDrawerManager.onNotifiableEventsReceived(notifiableEvents.filter { it !is NotifiableRingingCallEvent })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import timber.log.Timber
|
|||
import javax.inject.Inject
|
||||
|
||||
interface OnRedactedEventReceived {
|
||||
fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction)
|
||||
fun onRedactedEventsReceived(redactions: List<ResolvedPushEvent.Redaction>)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
|
|
@ -40,48 +40,54 @@ class DefaultOnRedactedEventReceived @Inject constructor(
|
|||
@ApplicationContext private val context: Context,
|
||||
private val stringProvider: StringProvider,
|
||||
) : OnRedactedEventReceived {
|
||||
override fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) {
|
||||
override fun onRedactedEventsReceived(redactions: List<ResolvedPushEvent.Redaction>) {
|
||||
coroutineScope.launch {
|
||||
val notifications = activeNotificationsProvider.getMessageNotificationsForRoom(
|
||||
redaction.sessionId,
|
||||
redaction.roomId,
|
||||
)
|
||||
if (notifications.isEmpty()) {
|
||||
Timber.d("No notifications found for redacted event")
|
||||
val redactionsBySessionIdAndRoom = redactions.groupBy { redaction ->
|
||||
redaction.sessionId to redaction.roomId
|
||||
}
|
||||
notifications.forEach { statusBarNotification ->
|
||||
val notification = statusBarNotification.notification
|
||||
val messagingStyle = MessagingStyle.extractMessagingStyleFromNotification(notification)
|
||||
if (messagingStyle == null) {
|
||||
Timber.w("Unable to retrieve messaging style from notification")
|
||||
return@forEach
|
||||
for ((keys, roomRedactions) in redactionsBySessionIdAndRoom) {
|
||||
val (sessionId, roomId) = keys
|
||||
val notifications = activeNotificationsProvider.getMessageNotificationsForRoom(
|
||||
sessionId,
|
||||
roomId,
|
||||
)
|
||||
if (notifications.isEmpty()) {
|
||||
Timber.d("No notifications found for redacted event")
|
||||
}
|
||||
val messageToRedactIndex = messagingStyle.messages.indexOfFirst { message ->
|
||||
message.extras.getString(DefaultNotificationCreator.MESSAGE_EVENT_ID) == redaction.redactedEventId.value
|
||||
}
|
||||
if (messageToRedactIndex == -1) {
|
||||
Timber.d("Unable to find the message to remove from notification")
|
||||
return@forEach
|
||||
}
|
||||
val oldMessage = messagingStyle.messages[messageToRedactIndex]
|
||||
val content = buildSpannedString {
|
||||
inSpans(StyleSpan(Typeface.ITALIC)) {
|
||||
append(stringProvider.getString(CommonStrings.common_message_removed))
|
||||
notifications.forEach { statusBarNotification ->
|
||||
val notification = statusBarNotification.notification
|
||||
val messagingStyle = MessagingStyle.extractMessagingStyleFromNotification(notification)
|
||||
if (messagingStyle == null) {
|
||||
Timber.w("Unable to retrieve messaging style from notification")
|
||||
return@forEach
|
||||
}
|
||||
val messageToRedactIndex = messagingStyle.messages.indexOfFirst { message ->
|
||||
roomRedactions.any { it.redactedEventId.value == message.extras.getString(DefaultNotificationCreator.MESSAGE_EVENT_ID) }
|
||||
}
|
||||
if (messageToRedactIndex == -1) {
|
||||
Timber.d("Unable to find the message to remove from notification")
|
||||
return@forEach
|
||||
}
|
||||
val oldMessage = messagingStyle.messages[messageToRedactIndex]
|
||||
val content = buildSpannedString {
|
||||
inSpans(StyleSpan(Typeface.ITALIC)) {
|
||||
append(stringProvider.getString(CommonStrings.common_message_removed))
|
||||
}
|
||||
}
|
||||
val newMessage = MessagingStyle.Message(
|
||||
content,
|
||||
oldMessage.timestamp,
|
||||
oldMessage.person
|
||||
)
|
||||
messagingStyle.messages[messageToRedactIndex] = newMessage
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
statusBarNotification.tag,
|
||||
statusBarNotification.id,
|
||||
NotificationCompat.Builder(context, notification)
|
||||
.setStyle(messagingStyle)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
val newMessage = MessagingStyle.Message(
|
||||
content,
|
||||
oldMessage.timestamp,
|
||||
oldMessage.person
|
||||
)
|
||||
messagingStyle.messages[messageToRedactIndex] = newMessage
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
statusBarNotification.tag,
|
||||
statusBarNotification.id,
|
||||
NotificationCompat.Builder(context, notification)
|
||||
.setStyle(messagingStyle)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@
|
|||
package io.element.android.libraries.push.impl.push
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.coroutine.parallelMap
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
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.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -30,42 +30,44 @@ class SyncOnNotifiableEvent @Inject constructor(
|
|||
private val featureFlagService: FeatureFlagService,
|
||||
private val appForegroundStateService: AppForegroundStateService,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
) {
|
||||
suspend operator fun invoke(notifiableEvent: NotifiableEvent) = withContext(dispatchers.io) {
|
||||
val isRingingCallEvent = notifiableEvent is NotifiableRingingCallEvent
|
||||
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush) && !isRingingCallEvent) {
|
||||
suspend operator fun invoke(notifiableEvents: List<NotifiableEvent>) = withContext(dispatchers.io) {
|
||||
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush)) {
|
||||
return@withContext
|
||||
}
|
||||
val client = matrixClientProvider.getOrRestore(notifiableEvent.sessionId).getOrNull() ?: return@withContext
|
||||
|
||||
client.getJoinedRoom(notifiableEvent.roomId)?.use { room ->
|
||||
room.subscribeToSync()
|
||||
try {
|
||||
val eventsBySession = notifiableEvents.groupBy { it.sessionId }
|
||||
|
||||
// If the app is in foreground, sync is already running, so we just add the subscription above.
|
||||
if (!appForegroundStateService.isInForeground.value) {
|
||||
if (isRingingCallEvent) {
|
||||
room.waitsUntilUserIsInTheCall(timeout = 60.seconds)
|
||||
} else {
|
||||
try {
|
||||
appForegroundStateService.updateIsSyncingNotificationEvent(true)
|
||||
room.waitsUntilEventIsKnown(eventId = notifiableEvent.eventId, timeout = 10.seconds)
|
||||
} finally {
|
||||
appForegroundStateService.updateIsSyncingNotificationEvent(false)
|
||||
appForegroundStateService.updateIsSyncingNotificationEvent(true)
|
||||
|
||||
for ((sessionId, events) in eventsBySession) {
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: continue
|
||||
val eventsByRoomId = events.groupBy { it.roomId }
|
||||
|
||||
client.roomListService.subscribeToVisibleRooms(eventsByRoomId.keys.toList())
|
||||
|
||||
if (!appForegroundStateService.isInForeground.value) {
|
||||
for ((roomId, eventsInRoom) in eventsByRoomId) {
|
||||
val activeRoom = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
|
||||
val room = activeRoom ?: client.getJoinedRoom(roomId)
|
||||
|
||||
if (room != null) {
|
||||
eventsInRoom.parallelMap { event ->
|
||||
room.waitsUntilEventIsKnown(event.eventId, timeout = 10.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
if (room != null && activeRoom == null) {
|
||||
// Destroy the room we just instantiated to reset its live timeline
|
||||
room.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User can be in the call if they answer using another session.
|
||||
* If the user does not join the call, the timeout will be reached.
|
||||
*/
|
||||
private suspend fun BaseRoom.waitsUntilUserIsInTheCall(timeout: Duration) {
|
||||
withTimeoutOrNull(timeout) {
|
||||
roomInfoFlow.first {
|
||||
sessionId in it.activeRoomCallParticipants
|
||||
}
|
||||
} finally {
|
||||
appForegroundStateService.updateIsSyncingNotificationEvent(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,35 @@
|
|||
<string name="notification_channel_noisy">"Hysbysiadau swnllyd"</string>
|
||||
<string name="notification_channel_ringing_calls">"Galwadau\'n canu"</string>
|
||||
<string name="notification_channel_silent">"Hysbysiadau tawel"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="zero">"%1$s: %2$d negeseuon"</item>
|
||||
<item quantity="one">"%1$s: %2$d neges"</item>
|
||||
<item quantity="two">"%1$s: %2$d neges"</item>
|
||||
<item quantity="few">"%1$s: %2$d neges"</item>
|
||||
<item quantity="many">"%1$s: %2$d neges"</item>
|
||||
<item quantity="other">"%1$s: %2$d neges"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_compat_summary_title">
|
||||
<item quantity="zero">"%d hysbysiadau"</item>
|
||||
<item quantity="one">"%d hysbysiad"</item>
|
||||
<item quantity="two">"%d hysbysiad"</item>
|
||||
<item quantity="few">"%d hysbysiad"</item>
|
||||
<item quantity="many">"%d hysbysiad"</item>
|
||||
<item quantity="other">"%d hysbysiad"</item>
|
||||
</plurals>
|
||||
<string name="notification_fallback_content">"Hysbysiad"</string>
|
||||
<string name="notification_incoming_call">"📹 Galwad i mewn"</string>
|
||||
<string name="notification_inline_reply_failed">"** Wedi methu anfon - agorwch yr ystafell"</string>
|
||||
<string name="notification_invitation_action_join">"Ymuno"</string>
|
||||
<string name="notification_invitation_action_reject">"Gwrthod"</string>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="zero">"%d gwahoddiadau"</item>
|
||||
<item quantity="one">"%d gwahoddiadau"</item>
|
||||
<item quantity="two">"%d gwahoddiadau"</item>
|
||||
<item quantity="few">"%d gwahoddiadau"</item>
|
||||
<item quantity="many">"%d gwahoddiadau"</item>
|
||||
<item quantity="other">"%d gwahoddiadau"</item>
|
||||
</plurals>
|
||||
<string name="notification_invite_body">"Wedi eich gwahodd i sgwrsio"</string>
|
||||
<string name="notification_invite_body_with_sender">"Mae %1$s wedi eich gwahodd i sgwrsio"</string>
|
||||
<string name="notification_mentioned_you_body">"Wedi eich crybwyll: %1$s"</string>
|
||||
|
|
@ -32,9 +56,25 @@
|
|||
<string name="notification_test_push_notification_content">"Rydych chi\'n edrych ar yr hysbysiad! Cliciwch fi!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s : %2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
<item quantity="zero">"%d negeseuon hysbyswyd heb eu darllen"</item>
|
||||
<item quantity="one">"%d neges hysbyswyd heb ei ddarllen"</item>
|
||||
<item quantity="two">"%d neges hysbyswyd heb eu darllen"</item>
|
||||
<item quantity="few">"%d neges hysbyswyd heb eu darllen"</item>
|
||||
<item quantity="many">"%d neges hysbyswyd heb eu darllen"</item>
|
||||
<item quantity="other">"%d neges hysbyswyd heb eu darllen"</item>
|
||||
</plurals>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s a %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room">"%1$s yn %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s yn %2$s a %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="zero">"%d ystafelloedd"</item>
|
||||
<item quantity="one">"%d ystafell"</item>
|
||||
<item quantity="two">"%d ystafell"</item>
|
||||
<item quantity="few">"%d ystafell"</item>
|
||||
<item quantity="many">"%d ystafell"</item>
|
||||
<item quantity="other">"%d ystafell"</item>
|
||||
</plurals>
|
||||
<string name="push_distributor_background_sync_android">"Cydweddu\'n y cefndir"</string>
|
||||
<string name="push_distributor_firebase_android">"Gwasanaethau Google"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Heb ganfod Google Play Services dilys. Efallai fydd hysbysiadau ddim yn gweithio\'n iawn."</string>
|
||||
|
|
@ -44,6 +84,14 @@
|
|||
<string name="troubleshoot_notifications_test_current_push_provider_title">"Darparwr gwthio presennol"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Gwnewch yn siŵr fod y cais yn cefnogi o leiaf un darparwr gwthio."</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Heb ganfod cefnogaeth darparwr gwthio."</string>
|
||||
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
|
||||
<item quantity="zero">"Wedi canfod %1$d darparwyr gwthio: %2$s"</item>
|
||||
<item quantity="one">"Wedi canfod %1$d darparwr gwthio: %2$s"</item>
|
||||
<item quantity="two">"Wedi canfod %1$d darparwr gwthio: %2$s"</item>
|
||||
<item quantity="few">"Wedi canfod %1$d darparwr gwthio: %2$s"</item>
|
||||
<item quantity="many">"Wedi canfod %1$d darparwr gwthio: %2$s"</item>
|
||||
<item quantity="other">"Wedi canfod %1$d darparwr gwthio: %2$s"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_success_2">"Adeiladwyd y rhaglen gyda chefnogaeth ar gyfer: %1$s"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Cefnogaeth darparwr gwthio"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_description">"Gwiriwch y gall y cais ddangos hysbysiad."</string>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
<item quantity="one">"%d Einladung"</item>
|
||||
<item quantity="other">"%d Einladungen"</item>
|
||||
</plurals>
|
||||
<string name="notification_invite_body">"Du wurdest zu einem Chat eingeladen"</string>
|
||||
<string name="notification_invite_body">"Sie wurden zu einem Chat eingeladen"</string>
|
||||
<string name="notification_invite_body_with_sender">"%1$s hat dich zum Chatten eingeladen"</string>
|
||||
<string name="notification_mentioned_you_body">"Hat Dich erwähnt: %1$s"</string>
|
||||
<string name="notification_new_messages">"Neue Nachrichten"</string>
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
<string name="notification_room_invite_body_with_sender">"%1$s hat dich eingeladen, dem Chatroom beizutreten"</string>
|
||||
<string name="notification_sender_me">"Ich"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s hat Dich erwähnt oder geantwortet"</string>
|
||||
<string name="notification_test_push_notification_content">"Du siehst dir die Benachrichtigung an! Klicke hier!"</string>
|
||||
<string name="notification_test_push_notification_content">"Sie sehen die Benachrichtigung! Klicken Sie hier!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
|
|
@ -55,17 +55,17 @@
|
|||
<string name="push_distributor_firebase_android">"Google-Dienste"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_description">"Ermittele den Namen des aktuellen Anbieters."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Kein Push-Anbieter ausgewählt."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"Aktueller Push-Anbieter:%1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_title">"Aktueller Push-Anbieter"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Stelle sicher, dass die Anwendung mindestens einen Push-Anbieter hat."</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Keine Push-Anbieter gefunden."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Kein Pushanbieter ausgewählt."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"Aktueller Pushanbieter: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_title">"Aktueller Pushanbieter"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Stellen Sie sicher, dass die Anwendung mindestens einen Pushanbieter unterstützt."</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Keine Unterstützung für Pushanbieter gefunden."</string>
|
||||
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
|
||||
<item quantity="one">"%1$d Push-Anbieter gefunden: %2$s"</item>
|
||||
<item quantity="other">"%1$d Push-Anbieter gefunden: %2$s"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_success_2">"Die Anwendung wurde mit Unterstützung für Folgendes erstellt: %1$s"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Push-Anbieter erkennen"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_success_2">"Die Anwendung bietet Unterstützung für: %1$s"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Unterstützung für Pushanbieter"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_description">"Prüfe, ob die Anwendung Benachrichtigungen anzeigen kann."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_failure">"Die Benachrichtigung wurde nicht angeklickt."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Die Benachrichtigung kann nicht angezeigt werden."</string>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<string name="notification_channel_call">"Chamada"</string>
|
||||
<string name="notification_channel_listening_for_events">"Ouvindo eventos"</string>
|
||||
<string name="notification_channel_noisy">"Notificações barulhentas"</string>
|
||||
<string name="notification_channel_ringing_calls">"Chamadas tocando"</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 ao enviar - por favor, abra a sala"</string>
|
||||
<string name="notification_invitation_action_join">"Entrar"</string>
|
||||
<string name="notification_invitation_action_reject">"Recusar"</string>
|
||||
|
|
@ -34,6 +36,7 @@
|
|||
<string name="notification_room_invite_body">"Convidou você para entrar na sala"</string>
|
||||
<string name="notification_room_invite_body_with_sender">"%1$s te convidou para entrar na sala"</string>
|
||||
<string name="notification_sender_me">"Eu"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s mencionado ou respondido"</string>
|
||||
<string name="notification_test_push_notification_content">"Você está visualizando a notificação! Clique em mim!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
|
|
@ -51,5 +54,29 @@
|
|||
<string name="push_distributor_background_sync_android">"Sincronização em segundo plano"</string>
|
||||
<string name="push_distributor_firebase_android">"Serviços do Google"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"O Google Play Services não foi encontrado. As notificações podem não funcionar corretamente."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_description">"Obtenha o nome do provedor atual."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Nenhum provedor de push foi selecionado."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"Provedor de push atual: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_title">"Provedor de push atual"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Certifique-se de que o aplicativo seja compatível com pelo menos um provedor de push."</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Nenhum suporte de provedor de push foi encontrado."</string>
|
||||
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
|
||||
<item quantity="one">"Encontrado %1$d provedor de push: %2$s"</item>
|
||||
<item quantity="other">"Encontrados %1$d provedores de push: %2$s"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_success_2">"O aplicativo foi desenvolvido com suporte para: %1$s"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Suporte ao provedor de push"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_description">"Verifique se o aplicativo pode exibir a notificação."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_failure">"A notificação não foi clicada."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Não é possível exibir a notificação."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_success">"A notificação foi clicada!"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_title">"Exibir notificação"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_waiting">"Clique na notificação para continuar o teste."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_description">"Certifique-se de que o aplicativo está recebendo push."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_1">"Erro: o pusher rejeitou a solicitação."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Erro: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"Erro, não é possível testar o push."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"Erro, tempo limite de espera pelo push."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_success">"O push loop back levou %1$d ms."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_title">"Teste o loopback do push"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@
|
|||
<item quantity="one">"%1$d fornecedor de envio encontrado: %2$s"</item>
|
||||
<item quantity="other">"%1$d fornecedores de envio encontrados: %2$s"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_success_2">"A aplicação suporta: %1$s"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Detetar fornecedores de envio"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_description">"Verificar se a aplicação consegue mostrar notificações."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_failure">"Não clicaste na notificação."</string>
|
||||
|
|
|
|||
78
libraries/push/impl/src/main/res/values-ur/translations.xml
Normal file
78
libraries/push/impl/src/main/res/values-ur/translations.xml
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<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>
|
||||
<item quantity="other">"%1$s: %2$d پیغامات"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_compat_summary_title">
|
||||
<item quantity="one">"%d اطلاع"</item>
|
||||
<item quantity="other">"%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>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="one">"%d دعوت نامہ"</item>
|
||||
<item quantity="other">"%d دعوت نامے"</item>
|
||||
</plurals>
|
||||
<string name="notification_invite_body">"آپ کو گفتگو کیلئے مدعو کیا"</string>
|
||||
<string name="notification_mentioned_you_body">"آپ کا تذکرہ کیا: %1$s"</string>
|
||||
<string name="notification_new_messages">"نئے پیغامات"</string>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
<item quantity="one">"%d نیا پیغام"</item>
|
||||
<item quantity="other">"%d نئے پیغامات"</item>
|
||||
</plurals>
|
||||
<string name="notification_reaction_body">"%1$s کے ساتھ ردعمل کیا"</string>
|
||||
<string name="notification_room_action_mark_as_read">"بطور مقروءہ نشانزد کریں"</string>
|
||||
<string name="notification_room_action_quick_reply">"فوری جواب"</string>
|
||||
<string name="notification_room_invite_body">"آپ کو کمرے میں شامل ہونے کی دعوت دی"</string>
|
||||
<string name="notification_sender_me">"میں"</string>
|
||||
<string name="notification_test_push_notification_content">"آپ اطلاع دیکھ رہے ہیں! مجھے دبائیں!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
<item quantity="one">"%d غیر مقروءہ مطلع پیغام"</item>
|
||||
<item quantity="other">"%d غیر مقروءہ مطلع پیغامات"</item>
|
||||
</plurals>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s اور %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room">"%2$s میں %1$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%2$s اور %3$s میں %1$s"</string>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="one">"%d کمرہ"</item>
|
||||
<item quantity="other">"%d کمرے"</item>
|
||||
</plurals>
|
||||
<string name="push_distributor_background_sync_android">"پس منظر مطابقت پذیری"</string>
|
||||
<string name="push_distributor_firebase_android">"گوگل سروسز"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"کوئی درست گوگل پلے سروسز نہیں ملی۔ ہو سکتا ہے اطلاعات ٹھیک سے کام نہ کریں۔"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_description">"موجودہ فراہم کنندہ کا نام حاصل کریں۔"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"کوئی دھکا فراہم کنندہ منتخب نہیں کیا گیا"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"موجودہ دھکا فراہم کنندہ: %1$s۔"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_title">"موجودہ دھکا فراہم کنندہ"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_description">"یقینی بنائیں کہ اطلاقیہ کم از کم ایک دھکا فراہم کنندہ کی حمایت کرتا ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"کوئی دھکا فراہم کنندہ حمایت نہیں ملی۔"</string>
|
||||
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
|
||||
<item quantity="one">"پایا %1$d دھکا فراہم کنندہ: %2$s"</item>
|
||||
<item quantity="other">"پائے %1$d دھکا فراہم کنندگان: %2$s"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_title">"دھکا فراہم کنندہ حمایت"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_description">"پڑتال کریں کہ اطلاقیہ اطلاع دکھا سکتا ہے"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_failure">"یہ اطلاع نہیں دبائی گئی"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"اطلاع ظاہر نہیں کرسکتا"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_success">"یہ اطلاع دبا دی گئی ہے!"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_title">"اطلاع دکھائیں"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_waiting">"برائے مہربانی جانچ جاری رکھنے کیلئے اطلاع پر دبائیں"</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_description">"اس بات کو یقینی بنائیں کہ اطلاقیے کو دھکا موصول ہو رہا ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_1">"نقص: دھکا کنندہ نے درخواست مسترد کردی ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"نقص: %1$s۔"</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"نقص، دھکا جانچ نہیں سکتا"</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"نقص، دھکے کے انتظار میں نفذ الوقت"</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_success">"دھکئی حلقۂ رجوعیہ نے %1$d مث لیا"</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_title">"دھکئی حلقۂ رجوعیہ جانچیں"</string>
|
||||
</resources>
|
||||
|
|
@ -9,6 +9,7 @@ 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.EventId
|
||||
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
|
||||
|
|
@ -67,7 +68,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
@Test
|
||||
fun `resolve event no session`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(notificationService = null)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")))
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
|
|
@ -76,36 +77,31 @@ class DefaultNotifiableEventResolverTest {
|
|||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.failure(AN_EXCEPTION)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve event null`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(null)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
assertThat(result.getEvent(request)?.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve event message text`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = TextMessageType(body = "Hello world", formatted = null)
|
||||
),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = TextMessageType(body = "Hello world", formatted = null)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Hello world")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -113,292 +109,337 @@ class DefaultNotifiableEventResolverTest {
|
|||
fun `resolve event message with mention`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = TextMessageType(body = "Hello world", formatted = null)
|
||||
),
|
||||
hasMention = true,
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = TextMessageType(body = "Hello world", formatted = null)
|
||||
),
|
||||
hasMention = true,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Hello world", hasMentionOrReply = true)
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve HTML formatted event message text takes plain text version`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = TextMessageType(
|
||||
body = "Hello world!",
|
||||
formatted = FormattedBody(
|
||||
body = "<b>Hello world</b>",
|
||||
format = MessageFormat.HTML,
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = TextMessageType(
|
||||
body = "Hello world!",
|
||||
formatted = FormattedBody(
|
||||
body = "<b>Hello world</b>",
|
||||
format = MessageFormat.HTML,
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Hello world")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve incorrectly formatted event message text uses fallback`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = TextMessageType(
|
||||
body = "Hello world",
|
||||
formatted = FormattedBody(
|
||||
body = "???Hello world!???",
|
||||
format = MessageFormat.UNKNOWN,
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = TextMessageType(
|
||||
body = "Hello world",
|
||||
formatted = FormattedBody(
|
||||
body = "???Hello world!???",
|
||||
format = MessageFormat.UNKNOWN,
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Hello world")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve event message audio`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = AudioMessageType("Audio", null, null, MediaSource("url"), null)
|
||||
),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = AudioMessageType("Audio", null, null, MediaSource("url"), null)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Audio")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve event message video`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = VideoMessageType("Video", null, null, MediaSource("url"), null)
|
||||
),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = VideoMessageType("Video", null, null, MediaSource("url"), null)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Video")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve event message voice`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = VoiceMessageType("Voice", null, null, MediaSource("url"), null, null)
|
||||
),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = VoiceMessageType("Voice", null, null, MediaSource("url"), null, null)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Voice message")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve event message image`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = ImageMessageType("Image", null, null, MediaSource("url"), null),
|
||||
),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = ImageMessageType("Image", null, null, MediaSource("url"), null),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Image")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve event message sticker`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = StickerMessageType("Sticker", null, null, MediaSource("url"), null),
|
||||
),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = StickerMessageType("Sticker", null, null, MediaSource("url"), null),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Sticker")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve event message file`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = FileMessageType("File", null, null, MediaSource("url"), null),
|
||||
),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = FileMessageType("File", null, null, MediaSource("url"), null),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "File")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve event message location`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = LocationMessageType("Location", "geo:1,2", null),
|
||||
),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = LocationMessageType("Location", "geo:1,2", null),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Location")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve event message notice`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = NoticeMessageType("Notice", null),
|
||||
),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = NoticeMessageType("Notice", null),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Notice")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve event message emote`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = EmoteMessageType("is happy", null),
|
||||
),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomMessage(
|
||||
senderId = A_USER_ID_2,
|
||||
messageType = EmoteMessageType("is happy", null),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "* Bob is happy")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve poll`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.Poll(
|
||||
senderId = A_USER_ID_2,
|
||||
question = "A question"
|
||||
),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.Poll(
|
||||
senderId = A_USER_ID_2,
|
||||
question = "A question"
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Poll: A question")
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve RoomMemberContent invite room`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.StateEvent.RoomMemberContent(
|
||||
userId = A_USER_ID_2,
|
||||
membershipState = RoomMembershipState.INVITE
|
||||
),
|
||||
isDirect = false,
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.StateEvent.RoomMemberContent(
|
||||
userId = A_USER_ID_2,
|
||||
membershipState = RoomMembershipState.INVITE
|
||||
),
|
||||
isDirect = false,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result.getOrNull()).isNull()
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
assertThat(result.getEvent(request)?.getOrNull()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve invite room`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.Invite(
|
||||
senderId = A_USER_ID_2,
|
||||
),
|
||||
isDirect = false,
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.Invite(
|
||||
senderId = A_USER_ID_2,
|
||||
),
|
||||
isDirect = false,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
InviteNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
|
|
@ -417,22 +458,25 @@ class DefaultNotifiableEventResolverTest {
|
|||
isUpdated = false,
|
||||
)
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve invite direct`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.Invite(
|
||||
senderId = A_USER_ID_2,
|
||||
),
|
||||
isDirect = true,
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.Invite(
|
||||
senderId = A_USER_ID_2,
|
||||
),
|
||||
isDirect = true,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
InviteNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
|
|
@ -451,23 +495,26 @@ class DefaultNotifiableEventResolverTest {
|
|||
isUpdated = false,
|
||||
)
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve invite direct, no display name`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.Invite(
|
||||
senderId = A_USER_ID_2,
|
||||
),
|
||||
isDirect = true,
|
||||
senderDisplayName = null,
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.Invite(
|
||||
senderId = A_USER_ID_2,
|
||||
),
|
||||
isDirect = true,
|
||||
senderDisplayName = null,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
InviteNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
|
|
@ -486,23 +533,26 @@ class DefaultNotifiableEventResolverTest {
|
|||
isUpdated = false,
|
||||
)
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve invite direct, ambiguous display name`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.Invite(
|
||||
senderId = A_USER_ID_2,
|
||||
),
|
||||
isDirect = false,
|
||||
senderIsNameAmbiguous = true,
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.Invite(
|
||||
senderId = A_USER_ID_2,
|
||||
),
|
||||
isDirect = false,
|
||||
senderIsNameAmbiguous = true,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
InviteNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
|
|
@ -521,35 +571,37 @@ class DefaultNotifiableEventResolverTest {
|
|||
isUpdated = false,
|
||||
)
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve RoomMemberContent other`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.StateEvent.RoomMemberContent(
|
||||
userId = A_USER_ID_2,
|
||||
membershipState = RoomMembershipState.JOIN
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.StateEvent.RoomMemberContent(
|
||||
userId = A_USER_ID_2,
|
||||
membershipState = RoomMembershipState.JOIN
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result.getOrNull()).isNull()
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
assertThat(result.getEvent(request)?.getOrNull()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve RoomEncrypted`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomEncrypted
|
||||
)
|
||||
mapOf(AN_EVENT_ID to aNotificationData(content = NotificationContent.MessageLike.RoomEncrypted))
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
FallbackNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
|
|
@ -563,19 +615,22 @@ class DefaultNotifiableEventResolverTest {
|
|||
timestamp = A_FAKE_TIMESTAMP,
|
||||
)
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve CallInvite`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.CallInvite(A_USER_ID_2),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.CallInvite(A_USER_ID_2),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
NotifiableMessageEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
|
|
@ -601,7 +656,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
isUpdated = false
|
||||
)
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -609,11 +664,13 @@ class DefaultNotifiableEventResolverTest {
|
|||
val callNotificationEventResolver = FakeCallNotificationEventResolver()
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.CallNotify(
|
||||
A_USER_ID_2,
|
||||
CallNotifyType.NOTIFY
|
||||
),
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.CallNotify(
|
||||
A_USER_ID_2,
|
||||
CallNotifyType.NOTIFY
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
callNotificationEventResolver = callNotificationEventResolver,
|
||||
|
|
@ -639,18 +696,21 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
callNotificationEventResolver.resolveEventLambda = { _, _, _ -> Result.success(expectedResult.notifiableEvent) }
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve RoomRedaction`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomRedaction(
|
||||
AN_EVENT_ID_2,
|
||||
A_REDACTION_REASON,
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomRedaction(
|
||||
AN_EVENT_ID_2,
|
||||
A_REDACTION_REASON,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -661,82 +721,91 @@ class DefaultNotifiableEventResolverTest {
|
|||
redactedEventId = AN_EVENT_ID_2,
|
||||
reason = A_REDACTION_REASON,
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve RoomRedaction with null redactedEventId should return null`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomRedaction(
|
||||
null,
|
||||
A_REDACTION_REASON,
|
||||
mapOf(
|
||||
AN_EVENT_ID to aNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomRedaction(
|
||||
null,
|
||||
A_REDACTION_REASON,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
assertThat(result.getEvent(request)?.getOrNull()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve null cases`() {
|
||||
testFailure(NotificationContent.MessageLike.CallAnswer)
|
||||
testFailure(NotificationContent.MessageLike.CallHangup)
|
||||
testFailure(NotificationContent.MessageLike.CallCandidates)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationReady)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationStart)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationCancel)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationAccept)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationKey)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationMac)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationDone)
|
||||
testFailure(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value))
|
||||
testFailure(NotificationContent.MessageLike.Sticker)
|
||||
testFailure(NotificationContent.StateEvent.PolicyRuleRoom)
|
||||
testFailure(NotificationContent.StateEvent.PolicyRuleServer)
|
||||
testFailure(NotificationContent.StateEvent.PolicyRuleUser)
|
||||
testFailure(NotificationContent.StateEvent.RoomAliases)
|
||||
testFailure(NotificationContent.StateEvent.RoomAvatar)
|
||||
testFailure(NotificationContent.StateEvent.RoomCanonicalAlias)
|
||||
testFailure(NotificationContent.StateEvent.RoomCreate)
|
||||
testFailure(NotificationContent.StateEvent.RoomEncryption)
|
||||
testFailure(NotificationContent.StateEvent.RoomGuestAccess)
|
||||
testFailure(NotificationContent.StateEvent.RoomHistoryVisibility)
|
||||
testFailure(NotificationContent.StateEvent.RoomJoinRules)
|
||||
testFailure(NotificationContent.StateEvent.RoomName)
|
||||
testFailure(NotificationContent.StateEvent.RoomPinnedEvents)
|
||||
testFailure(NotificationContent.StateEvent.RoomPowerLevels)
|
||||
testFailure(NotificationContent.StateEvent.RoomServerAcl)
|
||||
testFailure(NotificationContent.StateEvent.RoomThirdPartyInvite)
|
||||
testFailure(NotificationContent.StateEvent.RoomTombstone)
|
||||
testFailure(NotificationContent.StateEvent.RoomTopic(""))
|
||||
testFailure(NotificationContent.StateEvent.SpaceChild)
|
||||
testFailure(NotificationContent.StateEvent.SpaceParent)
|
||||
testNoResults(NotificationContent.MessageLike.CallAnswer)
|
||||
testNoResults(NotificationContent.MessageLike.CallHangup)
|
||||
testNoResults(NotificationContent.MessageLike.CallCandidates)
|
||||
testNoResults(NotificationContent.MessageLike.KeyVerificationReady)
|
||||
testNoResults(NotificationContent.MessageLike.KeyVerificationStart)
|
||||
testNoResults(NotificationContent.MessageLike.KeyVerificationCancel)
|
||||
testNoResults(NotificationContent.MessageLike.KeyVerificationAccept)
|
||||
testNoResults(NotificationContent.MessageLike.KeyVerificationKey)
|
||||
testNoResults(NotificationContent.MessageLike.KeyVerificationMac)
|
||||
testNoResults(NotificationContent.MessageLike.KeyVerificationDone)
|
||||
testNoResults(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value))
|
||||
testNoResults(NotificationContent.MessageLike.Sticker)
|
||||
testNoResults(NotificationContent.StateEvent.PolicyRuleRoom)
|
||||
testNoResults(NotificationContent.StateEvent.PolicyRuleServer)
|
||||
testNoResults(NotificationContent.StateEvent.PolicyRuleUser)
|
||||
testNoResults(NotificationContent.StateEvent.RoomAliases)
|
||||
testNoResults(NotificationContent.StateEvent.RoomAvatar)
|
||||
testNoResults(NotificationContent.StateEvent.RoomCanonicalAlias)
|
||||
testNoResults(NotificationContent.StateEvent.RoomCreate)
|
||||
testNoResults(NotificationContent.StateEvent.RoomEncryption)
|
||||
testNoResults(NotificationContent.StateEvent.RoomGuestAccess)
|
||||
testNoResults(NotificationContent.StateEvent.RoomHistoryVisibility)
|
||||
testNoResults(NotificationContent.StateEvent.RoomJoinRules)
|
||||
testNoResults(NotificationContent.StateEvent.RoomName)
|
||||
testNoResults(NotificationContent.StateEvent.RoomPinnedEvents)
|
||||
testNoResults(NotificationContent.StateEvent.RoomPowerLevels)
|
||||
testNoResults(NotificationContent.StateEvent.RoomServerAcl)
|
||||
testNoResults(NotificationContent.StateEvent.RoomThirdPartyInvite)
|
||||
testNoResults(NotificationContent.StateEvent.RoomTombstone)
|
||||
testNoResults(NotificationContent.StateEvent.RoomTopic(""))
|
||||
testNoResults(NotificationContent.StateEvent.SpaceChild)
|
||||
testNoResults(NotificationContent.StateEvent.SpaceParent)
|
||||
}
|
||||
|
||||
private fun testFailure(content: NotificationContent) = runTest {
|
||||
private fun testNoResults(content: NotificationContent) = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
content = content
|
||||
)
|
||||
mapOf(AN_EVENT_ID to aNotificationData(content = content))
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
|
||||
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
|
||||
assertThat(result.getEvent(request)?.getOrNull()).isNull()
|
||||
}
|
||||
|
||||
private fun Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>.getEvent(
|
||||
request: NotificationEventRequest
|
||||
): Result<ResolvedPushEvent>? {
|
||||
return getOrNull()?.get(request)
|
||||
}
|
||||
|
||||
private fun createDefaultNotifiableEventResolver(
|
||||
notificationService: FakeNotificationService? = FakeNotificationService(),
|
||||
notificationResult: Result<NotificationData?> = Result.success(null),
|
||||
notificationResult: Result<Map<EventId, NotificationData>> = Result.success(emptyMap()),
|
||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
callNotificationEventResolver: FakeCallNotificationEventResolver = FakeCallNotificationEventResolver(),
|
||||
): DefaultNotifiableEventResolver {
|
||||
val context = RuntimeEnvironment.getApplication() as Context
|
||||
notificationService?.givenGetNotificationResult(notificationResult)
|
||||
notificationService?.givenGetNotificationsResult(notificationResult)
|
||||
val matrixClientProvider = FakeMatrixClientProvider(getClient = {
|
||||
if (notificationService == null) {
|
||||
Result.failure(IllegalStateException("Client not found"))
|
||||
|
|
|
|||
|
|
@ -42,7 +42,9 @@ class DefaultOnMissedCallNotificationHandlerTest {
|
|||
// 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)))
|
||||
givenGetNotificationsResult(
|
||||
Result.success(mapOf(AN_EVENT_ID to aNotificationData(senderDisplayName = A_USER_NAME, senderIsNameAmbiguous = false)))
|
||||
)
|
||||
}
|
||||
Result.success(FakeMatrixClient(notificationService = notificationService))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,16 +7,18 @@
|
|||
|
||||
package io.element.android.libraries.push.impl.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.impl.notifications.model.ResolvedPushEvent
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeNotifiableEventResolver(
|
||||
private val notifiableEventResult: (SessionId, RoomId, EventId) -> Result<ResolvedPushEvent> = { _, _, _ -> lambdaError() }
|
||||
private val resolveEventsResult: (SessionId, List<NotificationEventRequest>) -> Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>> =
|
||||
{ _, _ -> lambdaError() }
|
||||
) : NotifiableEventResolver {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent> {
|
||||
return notifiableEventResult(sessionId, roomId, eventId)
|
||||
override suspend fun resolveEvents(
|
||||
sessionId: SessionId,
|
||||
notificationEventRequests: List<NotificationEventRequest>
|
||||
): Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>> {
|
||||
return resolveEventsResult(sessionId, notificationEventRequests)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven
|
|||
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.FakeNotificationCleaner
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
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
|
||||
|
|
@ -353,8 +354,8 @@ class NotificationBroadcastReceiverHandlerTest {
|
|||
)
|
||||
)
|
||||
}
|
||||
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> }
|
||||
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult)
|
||||
val onNotifiableEventsReceivedResult = lambdaRecorder<List<NotifiableEvent>, Unit> { _ -> }
|
||||
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceivedResult = onNotifiableEventsReceivedResult)
|
||||
val sut = createNotificationBroadcastReceiverHandler(
|
||||
joinedRoom = joinedRoom,
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
|
|
@ -370,7 +371,7 @@ class NotificationBroadcastReceiverHandlerTest {
|
|||
sendMessage.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_MESSAGE), value(null), value(emptyList<IntentionalMention>()))
|
||||
onNotifiableEventReceivedResult.assertions()
|
||||
onNotifiableEventsReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
replyMessage.assertions()
|
||||
.isNeverCalled()
|
||||
|
|
@ -420,8 +421,8 @@ class NotificationBroadcastReceiverHandlerTest {
|
|||
)
|
||||
)
|
||||
}
|
||||
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> }
|
||||
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult)
|
||||
val onNotifiableEventsReceivedResult = lambdaRecorder<List<NotifiableEvent>, Unit> { _ -> }
|
||||
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceivedResult = onNotifiableEventsReceivedResult)
|
||||
val sut = createNotificationBroadcastReceiverHandler(
|
||||
joinedRoom = joinedRoom,
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
|
|
@ -438,7 +439,7 @@ class NotificationBroadcastReceiverHandlerTest {
|
|||
runCurrent()
|
||||
sendMessage.assertions()
|
||||
.isNeverCalled()
|
||||
onNotifiableEventReceivedResult.assertions()
|
||||
onNotifiableEventsReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
replyMessage.assertions()
|
||||
.isCalledOnce()
|
||||
|
|
@ -477,6 +478,7 @@ class NotificationBroadcastReceiverHandlerTest {
|
|||
onNotifiableEventReceived: OnNotifiableEventReceived = FakeOnNotifiableEventReceived(),
|
||||
stringProvider: StringProvider = FakeStringProvider(),
|
||||
replyMessageExtractor: ReplyMessageExtractor = FakeReplyMessageExtractor(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
): NotificationBroadcastReceiverHandler {
|
||||
return NotificationBroadcastReceiverHandler(
|
||||
appCoroutineScope = this,
|
||||
|
|
@ -494,6 +496,7 @@ class NotificationBroadcastReceiverHandlerTest {
|
|||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
stringProvider = stringProvider,
|
||||
replyMessageExtractor = replyMessageExtractor,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class DefaultOnRedactedEventReceivedTest {
|
|||
val sut = createDefaultOnRedactedEventReceived(
|
||||
getMessageNotificationsForRoomResult = { _, _ -> emptyList() }
|
||||
)
|
||||
sut.onRedactedEventReceived(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null))
|
||||
sut.onRedactedEventsReceived(listOf(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null)))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -48,7 +48,7 @@ class DefaultOnRedactedEventReceivedTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
sut.onRedactedEventReceived(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null))
|
||||
sut.onRedactedEventsReceived(listOf(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null)))
|
||||
}
|
||||
|
||||
private fun TestScope.createDefaultOnRedactedEventReceived(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta
|
|||
import io.element.android.libraries.push.impl.history.FakePushHistoryService
|
||||
import io.element.android.libraries.push.impl.history.PushHistoryService
|
||||
import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationEventRequest
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationResolverQueue
|
||||
import io.element.android.libraries.push.impl.notifications.ResolvingException
|
||||
import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
|
||||
|
|
@ -48,11 +50,15 @@ import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.Fa
|
|||
import io.element.android.tests.testutils.lambda.any
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.matching
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
private const val A_PUSHER_INFO = "info"
|
||||
|
||||
|
|
@ -80,10 +86,11 @@ class DefaultPushHandlerTest {
|
|||
fun `when classical PushData is received, the notification drawer is informed`() = runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent>> { _, _, _ ->
|
||||
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))
|
||||
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _, ->
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
|
|
@ -96,8 +103,8 @@ class DefaultPushHandlerTest {
|
|||
clientSecret = A_SECRET,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = notifiableEventResult,
|
||||
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
||||
notifiableEventsResult = notifiableEventResult,
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
|
|
@ -105,14 +112,17 @@ class DefaultPushHandlerTest {
|
|||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
|
||||
advanceTimeBy(300.milliseconds)
|
||||
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
notifiableEventResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID))
|
||||
onNotifiableEventReceived.assertions()
|
||||
.with(value(A_USER_ID), any())
|
||||
onNotifiableEventsReceived.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(aNotifiableMessageEvent))
|
||||
.with(value(listOf(aNotifiableMessageEvent)))
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
}
|
||||
|
|
@ -122,10 +132,11 @@ class DefaultPushHandlerTest {
|
|||
runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ ->
|
||||
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))
|
||||
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val aPushData = PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
|
|
@ -138,8 +149,8 @@ class DefaultPushHandlerTest {
|
|||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = notifiableEventResult,
|
||||
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
||||
notifiableEventsResult = notifiableEventResult,
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
|
|
@ -150,11 +161,14 @@ class DefaultPushHandlerTest {
|
|||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
|
||||
advanceTimeBy(300.milliseconds)
|
||||
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
notifiableEventResult.assertions()
|
||||
.isCalledOnce()
|
||||
onNotifiableEventReceived.assertions()
|
||||
onNotifiableEventsReceived.assertions()
|
||||
.isNeverCalled()
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
|
|
@ -165,10 +179,11 @@ class DefaultPushHandlerTest {
|
|||
runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ ->
|
||||
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))
|
||||
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val aPushData = PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
|
|
@ -181,8 +196,8 @@ class DefaultPushHandlerTest {
|
|||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = notifiableEventResult,
|
||||
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
||||
notifiableEventsResult = notifiableEventResult,
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { null }
|
||||
),
|
||||
|
|
@ -193,14 +208,17 @@ class DefaultPushHandlerTest {
|
|||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
|
||||
advanceTimeBy(300.milliseconds)
|
||||
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
notifiableEventResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID))
|
||||
onNotifiableEventReceived.assertions()
|
||||
.with(value(A_USER_ID), any())
|
||||
onNotifiableEventsReceived.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(aNotifiableMessageEvent))
|
||||
.with(value(listOf(aNotifiableMessageEvent)))
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
}
|
||||
|
|
@ -210,10 +228,11 @@ class DefaultPushHandlerTest {
|
|||
runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ ->
|
||||
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))
|
||||
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val aPushData = PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
|
|
@ -226,8 +245,8 @@ class DefaultPushHandlerTest {
|
|||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = notifiableEventResult,
|
||||
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
||||
notifiableEventsResult = notifiableEventResult,
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { null }
|
||||
),
|
||||
|
|
@ -242,7 +261,7 @@ class DefaultPushHandlerTest {
|
|||
.isCalledOnce()
|
||||
notifiableEventResult.assertions()
|
||||
.isNeverCalled()
|
||||
onNotifiableEventReceived.assertions()
|
||||
onNotifiableEventsReceived.assertions()
|
||||
.isNeverCalled()
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
|
|
@ -252,10 +271,10 @@ class DefaultPushHandlerTest {
|
|||
fun `when classical PushData is received, but not able to resolve the event, nothing happen`() =
|
||||
runTest {
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ ->
|
||||
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
|
||||
Result.failure(ResolvingException("Unable to resolve"))
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val aPushData = PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
|
|
@ -268,8 +287,8 @@ class DefaultPushHandlerTest {
|
|||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = notifiableEventResult,
|
||||
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
||||
notifiableEventsResult = notifiableEventResult,
|
||||
buildMeta = aBuildMeta(
|
||||
// Also test `lowPrivacyLoggingEnabled = false` here
|
||||
lowPrivacyLoggingEnabled = false
|
||||
|
|
@ -281,12 +300,15 @@ class DefaultPushHandlerTest {
|
|||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
|
||||
advanceTimeBy(300.milliseconds)
|
||||
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
notifiableEventResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID))
|
||||
onNotifiableEventReceived.assertions()
|
||||
.with(value(A_USER_ID), any())
|
||||
onNotifiableEventsReceived.assertions()
|
||||
.isNeverCalled()
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
|
|
@ -313,28 +335,38 @@ class DefaultPushHandlerTest {
|
|||
Unit,
|
||||
> { _, _, _, _, _, _, _, _ -> }
|
||||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
notifiableEventResult = { _, _, _ ->
|
||||
notifiableEventsResult = { _, _ ->
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||
Result.success(
|
||||
ResolvedPushEvent.Event(aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()))
|
||||
mapOf(
|
||||
request to Result.success(
|
||||
ResolvedPushEvent.Event(
|
||||
aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli())
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
incrementPushCounterResult = {},
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
|
||||
advanceTimeBy(300.milliseconds)
|
||||
|
||||
handleIncomingCallLambda.assertions().isCalledOnce()
|
||||
onNotifiableEventReceived.assertions().isCalledOnce()
|
||||
onNotifiableEventsReceived.assertions().isNeverCalled()
|
||||
onPushReceivedResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
|
|
@ -346,7 +378,7 @@ class DefaultPushHandlerTest {
|
|||
unread = 0,
|
||||
clientSecret = A_SECRET,
|
||||
)
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
|
||||
val handleIncomingCallLambda = lambdaRecorder<
|
||||
CallType.RoomCall,
|
||||
EventId,
|
||||
|
|
@ -365,9 +397,10 @@ class DefaultPushHandlerTest {
|
|||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = { _, _, _ ->
|
||||
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY)))
|
||||
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
||||
notifiableEventsResult = { _, _ ->
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY)))))
|
||||
},
|
||||
incrementPushCounterResult = {},
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
|
|
@ -377,8 +410,10 @@ class DefaultPushHandlerTest {
|
|||
)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
|
||||
advanceTimeBy(300.milliseconds)
|
||||
|
||||
handleIncomingCallLambda.assertions().isNeverCalled()
|
||||
onNotifiableEventReceived.assertions().isCalledOnce()
|
||||
onNotifiableEventsReceived.assertions().isCalledOnce()
|
||||
onPushReceivedResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
|
|
@ -390,7 +425,7 @@ class DefaultPushHandlerTest {
|
|||
unread = 0,
|
||||
clientSecret = A_SECRET,
|
||||
)
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
|
||||
val handleIncomingCallLambda = lambdaRecorder<
|
||||
CallType.RoomCall,
|
||||
EventId,
|
||||
|
|
@ -409,9 +444,10 @@ class DefaultPushHandlerTest {
|
|||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = { _, _, _ ->
|
||||
Result.success(ResolvedPushEvent.Event(aNotifiableCallEvent()))
|
||||
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
||||
notifiableEventsResult = { _, _ ->
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableCallEvent()))))
|
||||
},
|
||||
incrementPushCounterResult = {},
|
||||
userPushStore = FakeUserPushStore().apply {
|
||||
|
|
@ -423,8 +459,11 @@ class DefaultPushHandlerTest {
|
|||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
|
||||
advanceTimeBy(300.milliseconds)
|
||||
|
||||
handleIncomingCallLambda.assertions().isCalledOnce()
|
||||
onNotifiableEventReceived.assertions().isCalledOnce()
|
||||
onNotifiableEventsReceived.assertions().isNeverCalled()
|
||||
onPushReceivedResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
|
|
@ -442,26 +481,32 @@ class DefaultPushHandlerTest {
|
|||
redactedEventId = AN_EVENT_ID_2,
|
||||
reason = null
|
||||
)
|
||||
val onRedactedEventReceived = lambdaRecorder<ResolvedPushEvent.Redaction, Unit> { }
|
||||
val onRedactedEventReceived = lambdaRecorder<List<ResolvedPushEvent.Redaction>, Unit> { }
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onRedactedEventReceived = onRedactedEventReceived,
|
||||
onRedactedEventsReceived = onRedactedEventReceived,
|
||||
incrementPushCounterResult = incrementPushCounterResult,
|
||||
notifiableEventResult = { _, _, _ -> Result.success(aRedaction) },
|
||||
notifiableEventsResult = { _, _ ->
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||
Result.success(mapOf(request to Result.success(aRedaction)))
|
||||
},
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
|
||||
advanceTimeBy(300.milliseconds)
|
||||
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
onRedactedEventReceived.assertions().isCalledOnce()
|
||||
.with(value(aRedaction))
|
||||
.with(value(listOf(aRedaction)))
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
}
|
||||
|
|
@ -493,10 +538,64 @@ class DefaultPushHandlerTest {
|
|||
.isCalledOnce()
|
||||
}
|
||||
|
||||
private fun createDefaultPushHandler(
|
||||
onNotifiableEventReceived: (NotifiableEvent) -> Unit = { lambdaError() },
|
||||
onRedactedEventReceived: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() },
|
||||
notifiableEventResult: (SessionId, RoomId, EventId) -> Result<ResolvedPushEvent> = { _, _, _ -> lambdaError() },
|
||||
@Test
|
||||
fun `when receiving several push notifications at the same time, those are batched before being processed`() = runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _, ->
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
|
||||
}
|
||||
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val aPushData = PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
unread = 0,
|
||||
clientSecret = A_SECRET,
|
||||
)
|
||||
val anotherPushData = PushData(
|
||||
eventId = AN_EVENT_ID_2,
|
||||
roomId = A_ROOM_ID,
|
||||
unread = 0,
|
||||
clientSecret = A_SECRET,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
||||
notifiableEventsResult = notifiableEventResult,
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
incrementPushCounterResult = incrementPushCounterResult,
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
defaultPushHandler.handle(anotherPushData, A_PUSHER_INFO)
|
||||
|
||||
advanceTimeBy(300.milliseconds)
|
||||
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledExactly(2)
|
||||
notifiableEventResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_USER_ID), matching<List<NotificationEventRequest>> { requests ->
|
||||
requests.size == 2 && requests.first().eventId == AN_EVENT_ID && requests.last().eventId == AN_EVENT_ID_2
|
||||
})
|
||||
onNotifiableEventsReceived.assertions()
|
||||
.isCalledOnce()
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledExactly(2)
|
||||
}
|
||||
|
||||
private fun TestScope.createDefaultPushHandler(
|
||||
onNotifiableEventsReceived: (List<NotifiableEvent>) -> Unit = { lambdaError() },
|
||||
onRedactedEventsReceived: (List<ResolvedPushEvent.Redaction>) -> Unit = { lambdaError() },
|
||||
notifiableEventsResult: (SessionId, List<NotificationEventRequest>) -> Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>> =
|
||||
{ _, _, -> lambdaError() },
|
||||
incrementPushCounterResult: () -> Unit = { lambdaError() },
|
||||
userPushStore: UserPushStore = FakeUserPushStore(),
|
||||
pushClientSecret: PushClientSecret = FakePushClientSecret(),
|
||||
|
|
@ -508,9 +607,8 @@ class DefaultPushHandlerTest {
|
|||
pushHistoryService: PushHistoryService = FakePushHistoryService(),
|
||||
): DefaultPushHandler {
|
||||
return DefaultPushHandler(
|
||||
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived),
|
||||
onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventReceived),
|
||||
notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventResult),
|
||||
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceived),
|
||||
onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventsReceived),
|
||||
incrementPushDataStore = object : IncrementPushDataStore {
|
||||
override suspend fun incrementPushCounter() {
|
||||
incrementPushCounterResult()
|
||||
|
|
@ -524,6 +622,8 @@ class DefaultPushHandlerTest {
|
|||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
notificationChannels = notificationChannels,
|
||||
pushHistoryService = pushHistoryService,
|
||||
resolverQueue = NotificationResolverQueue(notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventsResult), backgroundScope),
|
||||
appCoroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeOnNotifiableEventReceived(
|
||||
private val onNotifiableEventReceivedResult: (NotifiableEvent) -> Unit = { lambdaError() },
|
||||
private val onNotifiableEventsReceivedResult: (List<NotifiableEvent>) -> Unit = { lambdaError() },
|
||||
) : OnNotifiableEventReceived {
|
||||
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
|
||||
onNotifiableEventReceivedResult(notifiableEvent)
|
||||
override fun onNotifiableEventsReceived(notifiableEvents: List<NotifiableEvent>) {
|
||||
onNotifiableEventsReceivedResult(notifiableEvents)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeOnRedactedEventReceived(
|
||||
private val onRedactedEventReceivedResult: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() },
|
||||
private val onRedactedEventsReceivedResult: (List<ResolvedPushEvent.Redaction>) -> Unit = { lambdaError() },
|
||||
) : OnRedactedEventReceived {
|
||||
override fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) {
|
||||
onRedactedEventReceivedResult(redaction)
|
||||
override fun onRedactedEventsReceived(redactions: List<ResolvedPushEvent.Redaction>) {
|
||||
onRedactedEventsReceivedResult(redactions)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
|||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
|
|
@ -75,7 +76,7 @@ class SyncOnNotifiableEventTest {
|
|||
fun `when feature flag is disabled, nothing happens`() = runTest {
|
||||
val sut = createSyncOnNotifiableEvent(client = client, isSyncOnPushEnabled = false)
|
||||
|
||||
sut(notifiableEvent)
|
||||
sut(listOf(notifiableEvent))
|
||||
|
||||
assert(startSyncLambda).isNeverCalled()
|
||||
assert(stopSyncLambda).isNeverCalled()
|
||||
|
|
@ -96,7 +97,7 @@ class SyncOnNotifiableEventTest {
|
|||
unlocked.set(true)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||
}
|
||||
sut(incomingCallNotifiableEvent)
|
||||
sut(listOf(incomingCallNotifiableEvent))
|
||||
|
||||
// The process was completed before the timeout
|
||||
assertThat(unlocked.get()).isTrue()
|
||||
|
|
@ -116,30 +117,12 @@ class SyncOnNotifiableEventTest {
|
|||
unlocked.set(true)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||
}
|
||||
sut(incomingCallNotifiableEvent)
|
||||
sut(listOf(incomingCallNotifiableEvent))
|
||||
|
||||
// Didn't unlock before the timeout
|
||||
assertThat(unlocked.get()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when feature flag is enabled and app is in foreground, sync is not started`() = runTest {
|
||||
val appForegroundStateService = FakeAppForegroundStateService(
|
||||
initialForegroundValue = true,
|
||||
)
|
||||
val sut = createSyncOnNotifiableEvent(client = client, appForegroundStateService = appForegroundStateService, isSyncOnPushEnabled = true)
|
||||
|
||||
appForegroundStateService.isSyncingNotificationEvent.test {
|
||||
sut(notifiableEvent)
|
||||
sut(incomingCallNotifiableEvent)
|
||||
|
||||
// It's initially false
|
||||
assertThat(awaitItem()).isFalse()
|
||||
// It never becomes true
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when feature flag is enabled and app is in background, sync is started and stopped`() = runTest {
|
||||
val appForegroundStateService = FakeAppForegroundStateService(
|
||||
|
|
@ -153,7 +136,7 @@ class SyncOnNotifiableEventTest {
|
|||
|
||||
appForegroundStateService.isSyncingNotificationEvent.test {
|
||||
syncService.emitSyncState(SyncState.Running)
|
||||
sut(notifiableEvent)
|
||||
sut(listOf(notifiableEvent))
|
||||
|
||||
// It's initially false
|
||||
assertThat(awaitItem()).isFalse()
|
||||
|
|
@ -174,8 +157,8 @@ class SyncOnNotifiableEventTest {
|
|||
val sut = createSyncOnNotifiableEvent(client = client, appForegroundStateService = appForegroundStateService, isSyncOnPushEnabled = true)
|
||||
|
||||
appForegroundStateService.isSyncingNotificationEvent.test {
|
||||
launch { sut(notifiableEvent) }
|
||||
launch { sut(notifiableEvent) }
|
||||
launch { sut(listOf(notifiableEvent)) }
|
||||
launch { sut(listOf(notifiableEvent)) }
|
||||
launch {
|
||||
delay(1)
|
||||
timelineItems.emit(
|
||||
|
|
@ -199,7 +182,8 @@ class SyncOnNotifiableEventTest {
|
|||
isSyncOnPushEnabled: Boolean = true,
|
||||
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(
|
||||
initialForegroundValue = true,
|
||||
)
|
||||
),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
): SyncOnNotifiableEvent {
|
||||
val featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(
|
||||
|
|
@ -212,6 +196,7 @@ class SyncOnNotifiableEventTest {
|
|||
featureFlagService = featureFlagService,
|
||||
appForegroundStateService = appForegroundStateService,
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Certifique-se de que o Firebase esteja disponível."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"O Firebase não está disponível."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"O Firebase está disponível."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Verifique o Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Certifique-se de que o token do Firebase esteja disponível."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"O token do Firebase não é conhecido."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Token do Firebase: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Verifique o token do Firebase"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"یقینی بنائیں کہ Firebase دستیاب ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase دستیاب نہیں ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase دستیاب ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Firebase کی پڑتال کریں"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"یقینی بنائیں کہ Firebase رمزِ ممیز دستیاب ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase رمزِ ممیز معلوم نہیں ہے۔"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase رمزِ ممیز:%1$s۔"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Firebase رمزِ ممیز کی پڑتال کریں"</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue