Merge remote-tracking branch 'origin/develop' into task/eric/space-switching-unit-tests

# Conflicts:
#	vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
This commit is contained in:
ericdecanini 2022-07-21 10:48:57 +02:00
commit a909779e08
146 changed files with 1907 additions and 341 deletions

View File

@ -770,7 +770,7 @@ ij_kotlin_align_multiline_extends_list = false
ij_kotlin_align_multiline_method_parentheses = false ij_kotlin_align_multiline_method_parentheses = false
ij_kotlin_align_multiline_parameters = true ij_kotlin_align_multiline_parameters = true
ij_kotlin_align_multiline_parameters_in_calls = false ij_kotlin_align_multiline_parameters_in_calls = false
ij_kotlin_allow_trailing_comma = false ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = false ij_kotlin_allow_trailing_comma_on_call_site = false
ij_kotlin_assignment_wrap = off ij_kotlin_assignment_wrap = off
ij_kotlin_blank_lines_after_class_header = 0 ij_kotlin_blank_lines_after_class_header = 0

1
changelog.d/6200.bugfix Normal file
View File

@ -0,0 +1 @@
Fixes room not being in space after upgrade

1
changelog.d/6437.feature Normal file
View File

@ -0,0 +1 @@
[Location sharing] - Delete action on a live message

1
changelog.d/6487.feature Normal file
View File

@ -0,0 +1 @@
[Timeline] - Collapse redacted events

1
changelog.d/6537.bugfix Normal file
View File

@ -0,0 +1 @@
[Location Share] - Wrong room live location status bar visibility in timeline

1
changelog.d/6585.bugfix Normal file
View File

@ -0,0 +1 @@
Fix backup saving several times the same keys

1
changelog.d/6587.bugfix Normal file
View File

@ -0,0 +1 @@
Check user power level before sharing live location

1
changelog.d/6596.bugfix Normal file
View File

@ -0,0 +1 @@
[Location Share] - Live is considered as ended while still active

View File

@ -1 +1 @@
مُحادثة آمنة لا مركزية و VoIP. حافظ على بياناتك آمنة من الأطراف الثالثة. برنامج المراسلة الجماعية - الرسائل المشفرة والدردشة الجماعية ومكالمات الفيديو

View File

@ -1 +1 @@
Element (Riot.im سابقًا) إيليمنت - تطبيق محادثات أمن

View File

@ -0,0 +1,2 @@
Hlavní změny v této verzi: Podpora UnifiedPush a možnost používat push bez FCM.
Úplný seznam změn: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Hlavní změny v této verzi: Opravy různých chyb a vylepšení stability.
Úplný seznam změn: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Põhilised muutused selles versioonis: võimalus kasutada tõukesõnumite jaoks FCM'i asemel UnifiedPush'i.
Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Põhilised muutused selles versioonis: erinevate vigade parandused ja stabiilsust edendavad kohendused.
Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
تغییرات عمده در این نگارش: استفاده از UnifiedPush و اجازه به کاربر برای داشتن آگاهی‌های ارسالی بدون FCM.
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
تغییرات عمده در این نگارش: رفع اشکال‌های مختلف و بهبودهای پایداری.
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Principaux changements pour cette version : Utilisation de UnifiedPush qui permet aux utilisateur dutiliser « push » sans FCM.
Intégralité des changements : https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Principaux changements pour cette version : Plusieurs corrections de bogues et daméliorations de stabilité.
Intégralité des changements : https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Principais cambios nesta versión: melloras na xestión das mensaxes cifradas. Varios arranxos e melloras na estabilidade.
Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Principais cambios nesta versión: varios arranxos e melloras na estabilidade.
Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Principais cambios nesta versión: varios arranxos e melloras na estabilidade.
Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Principais cambios nesta versión: varios arranxos e melloras na estabilidade.
Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Principais cambios nesta versión: varios arranxos e melloras na estabilidade.
Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Principais cambios nesta versión: varios arranxos e melloras na estabilidade.
Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Principais cambios nesta versión: varios arranxos e melloras na estabilidade.
Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Principais cambios nesta versión: varios arranxos e melloras na estabilidade.
Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Principais cambios nesta versión: Utiliza UnifiedPush e permite á usuaria obter notificacións sen FCM.
Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Principais cambios nesta versión: varios arranxos e melloras na estabilidade.
Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,42 @@
Element é tanto unha mensaxería segura e unha app de productividade para o traballo en equipo, perfecta para conversas de grupos con traballo remoto. Esta app de chat usa cifrado de extremo-a-extremo para proporcionar video conferencias seguras, compartición de ficheiros e chamadas de voz.
<b>Características de Element incluídas:</b>
- Ferramentas avanzadas para a comunicación en liña
- Mensaxes completamente cifradas para permitir a comunicación corporativa, incluso para traballo remoto
- Chat descentralizado baseado no sistema de código aberto Matrix
- Compartición segura de ficheiros con datos cifrados na xestión de proxectos
- Chats de vídeo con Voz sobre IP en compartición de pantalla
- Integración doada con outras ferramentas de colaboración en liña, ferramentas de xestión de proxectos, servizos VoIP e outras apps de mensaxería para equipos
Element é completamente diferente a outras apps de mensaxería e traballo en equipo. Funciona grazas a Matrix, unha rede aberta para mensaxería segura e descentralizada. Permite a hospedaxe na infraestructura propia para proporcionar o maior grao de propiedade e control sobre os teus datos e mensaxes.
<b>Mensaxería privada e cifrada</b>
Element protéxete da publicidade non solicitada, minería de datos e burbullas de contido. Tamén protexe os teus datos, chamadas de vídeo e voz cifradas de extremo-a-extremo así como verificación con sinatura dos dispositivos.
Element pon baixo o teu control a túa privacidade permitíndoche comunicarte de xeito seguro con calquera a través da rede Matrix, ou en outras ferramentas de colaboración para empresas ao estar integrada en apps como Slack.
<b>Element na túa infraestructura</b>
Para un maior control sobre os teus datos sensibles e comunicacións, podes hospedar Element ou elexir calquera hóspede baseado en Matrix - un estándar para comunicación descentralizado e de código aberto. Element proporciona privacidade e seguridade así como flexibilidade para a integración.
<b>Os teus datos</b>
Ti decides onde gardas os teus datos e mensaxes. Sen o risco da minería de datos ou acceso por terceiras partes.
Element ponte ao mando de varios xeitos:
1. Consigue unha conta gratuíta no servidor público matrix.org hospedado polos desenvolvedores de Matrix, ou elixe entre miles de servidores públicos xestionados por voluntarias
2. Hospeda a túa conta na túa propia infraestructura IT
3. Crea unha conta nun servidor personalizado simplemente subscribíndote á plataforma de hospedaxe Element Matrix Services
<b>Mensaxería e Colaboración abertas</b>
Podes conversar con calquera na rede Matrix, tanto se usan Element ou outra app Matrix ou incluso unha mensaxería diferente.
<b>Super segura</b>
Cifrado real de extremo-a-extremo (só quen participa na conversa pode descifrar as mensaxes), e verificación con sinatura cruzada dos dispositivos.
<b>Comunicación e integración completas</b>
Mensaxería, chamadas de voz e vídeo, compartición de ficheiros, compartición de pantalla e moitas máis integracións, bots e widgets. Crea salas, comunidades, mantén o contacto e saca adiante o traballo.
<b>Continúa onde o deixaches</b>
Sigue en contacto alá onde estés grazas ao historial sincronizado de mensaxería entre tódolos dispositivos e na web https://app.element.io
<b>Código aberto</b>
Element Android é un proxecto de código aberto, hospedado en GitHub. Informa de fallos e/ou contribúe ao seu desenvolvemento en https://github.com/vector-im/element-android

View File

@ -0,0 +1 @@
Mensaxería en grupo - mensaxería cifrada, chat en grupo e videochamadas

View File

@ -0,0 +1 @@
Element - Mensaxería Segura

View File

@ -0,0 +1,2 @@
Perubahan utama dalam versi ini: Dukungan UnifiedPush, memungkinkan pengguna untuk diberitahukan tanpa FCM.
Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Perubahan utama dalam versi ini: Banyak perbaikan kutu dan perbaikan stabilitas.
Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Modifiche principali in questa versione: utilizza UnifiedPush e consente all'utente di avere notifiche push senza FCM.
Cronologia completa: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Modifiche principali in questa versione: varie correzioni di errori e miglioramenti della stabilità.
Cronologia completa: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Hlavné zmeny v tejto verzii: Použitie UnifiedPush a umožňuje používateľovi používať push bez FCM.
Úplný zoznam zmien: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Hlavné zmeny v tejto verzii: Rôzne opravy chýb a vylepšenia stability.
Úplný zoznam zmien: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Huvudsakliga ändringar i den här versionen: Använd UnifiedPush och tillåt användare att ha push utan FCM.
Full ändringslogg: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Huvudsakliga ändringar i den här versionen: Diverse buggfixar och stabilitetsförbättringar.
Full ändringslogg: https://github.com/vector-im/element-android/releases

View File

@ -1,2 +1,2 @@
Основні зміни в цій версії: поліпшення VoIP (аудіо та відео дзвінки в DM) та виправлення помилок! Основні зміни в цій версії: поліпшення VoIP (аудіо та відеовиклики у ПП) та виправлення помилок!
Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.1.0 Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.1.0

View File

@ -0,0 +1,2 @@
Основні зміни в цій версії: Застосовано UnifiedPush і дозволено користувачам отримувати push-сповіщення без FCM.
Перелік усіх змін: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
Основні зміни в цій версії: Усунуто різні вади й поліпшено стабільність.
Перелік усіх змін: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
此版本中的主要變動:使用 UnifiedPush 並允許使用者在沒有 FCM 的情況下推送。
完整的變更紀錄https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,2 @@
此版本中的主要變動:多個臭蟲修復與穩定性改善。
完整的變更紀錄https://github.com/vector-im/element-android/releases

View File

@ -199,7 +199,7 @@ dependencies {
implementation libs.apache.commonsImaging implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber // Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.51' implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.52'
testImplementation libs.tests.junit testImplementation libs.tests.junit
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281

View File

@ -24,7 +24,6 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -56,7 +55,6 @@ import java.util.concurrent.CountDownLatch
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@LargeTest @LargeTest
@Ignore
class KeysBackupTest : InstrumentedTest { class KeysBackupTest : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3) @get:Rule val rule = RetryTestRule(3)

View File

@ -202,7 +202,7 @@ data class Event(
* It will return a decrypted text message or an empty string otherwise. * It will return a decrypted text message or an empty string otherwise.
*/ */
fun getDecryptedTextSummary(): String? { fun getDecryptedTextSummary(): String? {
if (isRedacted()) return "Message Deleted" if (isRedacted()) return "Message removed"
val text = getDecryptedValue() ?: run { val text = getDecryptedValue() ?: run {
if (isPoll()) { if (isPoll()) {
return getPollQuestion() ?: "created a poll." return getPollQuestion() ?: "created a poll."
@ -371,6 +371,8 @@ fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClear
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO
fun Event.getRelationContent(): RelationDefaultContent? { fun Event.getRelationContent(): RelationDefaultContent? {
return if (isEncrypted()) { return if (isEncrypted()) {
content.toModel<EncryptedEventContent>()?.relatesTo content.toModel<EncryptedEventContent>()?.relatesTo

View File

@ -16,7 +16,6 @@
package org.matrix.android.sdk.api.session.room.location package org.matrix.android.sdk.api.session.room.location
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
@ -59,16 +58,21 @@ interface LocationSharingService {
*/ */
suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult
/**
* Redact (delete) the live associated to the given beacon info event id.
* @param beaconInfoEventId event id of the initial beacon info state event
* @param reason Optional reason string
*/
suspend fun redactLiveLocationShare(beaconInfoEventId: String, reason: String?)
/** /**
* Returns a LiveData on the list of current running live location shares. * Returns a LiveData on the list of current running live location shares.
*/ */
@MainThread
fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>>
/** /**
* Returns a LiveData on the live location share summary with the given eventId. * Returns a LiveData on the live location share summary with the given eventId.
* @param beaconInfoEventId event id of the initial beacon info state event * @param beaconInfoEventId event id of the initial beacon info state event
*/ */
@MainThread
fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData<Optional<LiveLocationShareAggregatedSummary>> fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData<Optional<LiveLocationShareAggregatedSummary>>
} }

View File

@ -33,5 +33,7 @@ enum class VersioningState {
/** /**
* The room has been upgraded, and the new room has been joined. * The room has been upgraded, and the new room has been joined.
*/ */
UPGRADED_ROOM_JOINED, UPGRADED_ROOM_JOINED;
fun isUpgraded() = this != NONE
} }

View File

@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.events.model.isEdition import org.matrix.android.sdk.api.session.events.model.isEdition
import org.matrix.android.sdk.api.session.events.model.isLiveLocation
import org.matrix.android.sdk.api.session.events.model.isPoll import org.matrix.android.sdk.api.session.events.model.isPoll
import org.matrix.android.sdk.api.session.events.model.isReply import org.matrix.android.sdk.api.session.events.model.isReply
import org.matrix.android.sdk.api.session.events.model.isSticker import org.matrix.android.sdk.api.session.events.model.isSticker
@ -165,6 +166,10 @@ fun TimelineEvent.isSticker(): Boolean {
return root.isSticker() return root.isSticker()
} }
fun TimelineEvent.isLiveLocation(): Boolean {
return root.isLiveLocation()
}
/** /**
* Returns whether or not the event is a root thread event. * Returns whether or not the event is a root thread event.
*/ */

View File

@ -37,9 +37,9 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData
* This class execute the registration request and is responsible to keep the session of interactive authentication. * This class execute the registration request and is responsible to keep the session of interactive authentication.
*/ */
internal class DefaultRegistrationWizard( internal class DefaultRegistrationWizard(
authAPI: AuthAPI, authAPI: AuthAPI,
private val sessionCreator: SessionCreator, private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore private val pendingSessionStore: PendingSessionStore
) : RegistrationWizard { ) : RegistrationWizard {
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
@ -74,20 +74,20 @@ internal class DefaultRegistrationWizard(
initialDeviceDisplayName: String? initialDeviceDisplayName: String?
): RegistrationResult { ): RegistrationResult {
val params = RegistrationParams( val params = RegistrationParams(
username = userName, username = userName,
password = password, password = password,
initialDeviceDisplayName = initialDeviceDisplayName initialDeviceDisplayName = initialDeviceDisplayName
) )
return performRegistrationRequest(params, LoginType.PASSWORD) return performRegistrationRequest(params, LoginType.PASSWORD)
.also { .also {
pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
.also { pendingSessionStore.savePendingSessionData(it) } .also { pendingSessionStore.savePendingSessionData(it) }
} }
} }
override suspend fun performReCaptcha(response: String): RegistrationResult { override suspend fun performReCaptcha(response: String): RegistrationResult {
val safeSession = pendingSessionData.currentSession val safeSession = pendingSessionData.currentSession
?: throw IllegalStateException("developer error, call createAccount() method first") ?: throw IllegalStateException("developer error, call createAccount() method first")
val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response)) val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response))
return performRegistrationRequest(params, LoginType.PASSWORD) return performRegistrationRequest(params, LoginType.PASSWORD)
@ -95,7 +95,7 @@ internal class DefaultRegistrationWizard(
override suspend fun acceptTerms(): RegistrationResult { override suspend fun acceptTerms(): RegistrationResult {
val safeSession = pendingSessionData.currentSession val safeSession = pendingSessionData.currentSession
?: throw IllegalStateException("developer error, call createAccount() method first") ?: throw IllegalStateException("developer error, call createAccount() method first")
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession)) val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession))
return performRegistrationRequest(params, LoginType.PASSWORD) return performRegistrationRequest(params, LoginType.PASSWORD)
@ -103,14 +103,14 @@ internal class DefaultRegistrationWizard(
override suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult { override suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult {
pendingSessionData = pendingSessionData.copy(currentThreePidData = null) pendingSessionData = pendingSessionData.copy(currentThreePidData = null)
.also { pendingSessionStore.savePendingSessionData(it) } .also { pendingSessionStore.savePendingSessionData(it) }
return sendThreePid(threePid) return sendThreePid(threePid)
} }
override suspend fun sendAgainThreePid(): RegistrationResult { override suspend fun sendAgainThreePid(): RegistrationResult {
val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid
?: throw IllegalStateException("developer error, call createAccount() method first") ?: throw IllegalStateException("developer error, call createAccount() method first")
return sendThreePid(safeCurrentThreePid) return sendThreePid(safeCurrentThreePid)
} }
@ -126,7 +126,7 @@ internal class DefaultRegistrationWizard(
) )
pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
.also { pendingSessionStore.savePendingSessionData(it) } .also { pendingSessionStore.savePendingSessionData(it) }
val params = RegistrationParams( val params = RegistrationParams(
auth = if (threePid is RegisterThreePid.Email) { auth = if (threePid is RegisterThreePid.Email) {
@ -149,7 +149,7 @@ internal class DefaultRegistrationWizard(
) )
// Store data // Store data
pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params)) pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params))
.also { pendingSessionStore.savePendingSessionData(it) } .also { pendingSessionStore.savePendingSessionData(it) }
// and send the sid a first time // and send the sid a first time
return performRegistrationRequest(params, LoginType.PASSWORD) return performRegistrationRequest(params, LoginType.PASSWORD)
@ -157,7 +157,7 @@ internal class DefaultRegistrationWizard(
override suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult { override suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult {
val safeParam = pendingSessionData.currentThreePidData?.registrationParams val safeParam = pendingSessionData.currentThreePidData?.registrationParams
?: throw IllegalStateException("developer error, no pending three pid") ?: throw IllegalStateException("developer error, no pending three pid")
return performRegistrationRequest(safeParam, LoginType.PASSWORD, delayMillis) return performRegistrationRequest(safeParam, LoginType.PASSWORD, delayMillis)
} }
@ -168,13 +168,13 @@ internal class DefaultRegistrationWizard(
private suspend fun validateThreePid(code: String): RegistrationResult { private suspend fun validateThreePid(code: String): RegistrationResult {
val registrationParams = pendingSessionData.currentThreePidData?.registrationParams val registrationParams = pendingSessionData.currentThreePidData?.registrationParams
?: throw IllegalStateException("developer error, no pending three pid") ?: throw IllegalStateException("developer error, no pending three pid")
val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first") val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first")
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code") val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code")
val validationBody = ValidationCodeBody( val validationBody = ValidationCodeBody(
clientSecret = pendingSessionData.clientSecret, clientSecret = pendingSessionData.clientSecret,
sid = safeCurrentData.addThreePidRegistrationResponse.sid, sid = safeCurrentData.addThreePidRegistrationResponse.sid,
code = code code = code
) )
val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody)) val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody))
if (validationResponse.isSuccess()) { if (validationResponse.isSuccess()) {
@ -189,7 +189,7 @@ internal class DefaultRegistrationWizard(
override suspend fun dummy(): RegistrationResult { override suspend fun dummy(): RegistrationResult {
val safeSession = pendingSessionData.currentSession val safeSession = pendingSessionData.currentSession
?: throw IllegalStateException("developer error, call createAccount() method first") ?: throw IllegalStateException("developer error, call createAccount() method first")
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession)) val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession))
return performRegistrationRequest(params, LoginType.PASSWORD) return performRegistrationRequest(params, LoginType.PASSWORD)

View File

@ -21,13 +21,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import timber.log.Timber import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
import javax.inject.Inject import javax.inject.Inject
internal data class InboundGroupSessionHolder( internal data class InboundGroupSessionHolder(
@ -57,18 +54,13 @@ internal class InboundGroupSessionStore @Inject constructor(
if (oldValue != null) { if (oldValue != null) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}") Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper }) // store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
oldValue.wrapper.session.releaseSession() oldValue.wrapper.session.releaseSession()
} }
} }
} }
} }
private val timer = Timer()
private var timerTask: TimerTask? = null
private val dirtySession = mutableListOf<InboundGroupSessionHolder>()
@Synchronized @Synchronized
fun clear() { fun clear() {
sessionCache.evictAll() sessionCache.evictAll()
@ -90,7 +82,6 @@ internal class InboundGroupSessionStore @Inject constructor(
@Synchronized @Synchronized
fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) { fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}") Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
dirtySession.remove(old)
store.removeInboundGroupSession(sessionId, senderKey) store.removeInboundGroupSession(sessionId, senderKey)
sessionCache.remove(CacheKey(sessionId, senderKey)) sessionCache.remove(CacheKey(sessionId, senderKey))
@ -107,33 +98,14 @@ internal class InboundGroupSessionStore @Inject constructor(
private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}") Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}")
// We want to batch this a bit for performances
dirtySession.add(holder)
if (sessionCache[CacheKey(sessionId, senderKey)] == null) { if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
// first time seen, put it in memory cache while waiting for batch insert // first time seen, put it in memory cache while waiting for batch insert
// If it's already known, no need to update cache it's already there // If it's already known, no need to update cache it's already there
sessionCache.put(CacheKey(sessionId, senderKey), holder) sessionCache.put(CacheKey(sessionId, senderKey), holder)
} }
timerTask?.cancel()
timerTask = object : TimerTask() {
override fun run() {
batchSave()
}
}
timer.schedule(timerTask!!, 300)
}
@Synchronized
private fun batchSave() {
val toSave = mutableListOf<InboundGroupSessionHolder>().apply { addAll(dirtySession) }
dirtySession.clear()
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}") store.storeInboundGroupSessions(listOf(holder.wrapper))
tryOrNull {
store.storeInboundGroupSessions(toSave.map { it.wrapper })
}
} }
} }
} }

View File

@ -604,14 +604,16 @@ internal class MXOlmDevice @Inject constructor(
* @param sharedHistory MSC3061, this key is sharable on invite * @param sharedHistory MSC3061, this key is sharable on invite
* @return true if the operation succeeds. * @return true if the operation succeeds.
*/ */
fun addInboundGroupSession(sessionId: String, fun addInboundGroupSession(
sessionKey: String, sessionId: String,
roomId: String, sessionKey: String,
senderKey: String, roomId: String,
forwardingCurve25519KeyChain: List<String>, senderKey: String,
keysClaimed: Map<String, String>, forwardingCurve25519KeyChain: List<String>,
exportFormat: Boolean, keysClaimed: Map<String, String>,
sharedHistory: Boolean): AddSessionResult { exportFormat: Boolean,
sharedHistory: Boolean
): AddSessionResult {
val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") { val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") {
if (exportFormat) { if (exportFormat) {
OlmInboundGroupSession.importSession(sessionKey) OlmInboundGroupSession.importSession(sessionKey)
@ -701,8 +703,8 @@ internal class MXOlmDevice @Inject constructor(
val senderKey = megolmSessionData.senderKey ?: continue val senderKey = megolmSessionData.senderKey ?: continue
val roomId = megolmSessionData.roomId val roomId = megolmSessionData.roomId
val candidateSessionToImport = try { val candidateSessionToImport = try {
MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true) MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true)
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId") Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId")
continue continue
@ -806,7 +808,6 @@ internal class MXOlmDevice @Inject constructor(
} }
replayAttackMap[messageIndexKey] = eventId replayAttackMap[messageIndexKey] = eventId
} }
inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
val payload = try { val payload = try {
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE) val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)

View File

@ -38,6 +38,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
outgoingKeyRequestManager, outgoingKeyRequestManager,
cryptoStore, cryptoStore,
matrixConfiguration, matrixConfiguration,
eventsManager) eventsManager
)
} }
} }

View File

@ -250,8 +250,10 @@ internal class MXMegolmEncryption(
* @param sessionInfo the session info * @param sessionInfo the session info
* @param devicesByUser the devices map * @param devicesByUser the devices map
*/ */
private suspend fun shareUserDevicesKey(sessionInfo: MXOutboundSessionInfo, private suspend fun shareUserDevicesKey(
devicesByUser: Map<String, List<CryptoDeviceInfo>>) { sessionInfo: MXOutboundSessionInfo,
devicesByUser: Map<String, List<CryptoDeviceInfo>>
) {
val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also { val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also {
Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export") Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export")
} }

View File

@ -1349,6 +1349,8 @@ internal class DefaultKeysBackupService @Inject constructor(
// Mark keys as backed up // Mark keys as backed up
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
// we can release the sessions now
olmInboundGroupSessionWrappers.onEach { it.session.releaseSession() }
if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) { if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) {
Timber.v("backupKeys: All keys have been backed up") Timber.v("backupKeys: All keys have been backed up")

View File

@ -763,11 +763,17 @@ internal class RealmCryptoStore @Inject constructor(
// } ?: false // } ?: false
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, wrapper.sessionData.senderKey) val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, wrapper.sessionData.senderKey)
val existing = realm.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findFirst()
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
primaryKey = key primaryKey = key
store(wrapper) store(wrapper)
backedUp = existing?.backedUp ?: false
} }
Timber.i("## CRYPTO | shouldShareHistory: ${wrapper.sessionData.sharedHistory} for $key")
Timber.v("## CRYPTO | shouldShareHistory: ${wrapper.sessionData.sharedHistory} for $key")
realm.insertOrUpdate(realmOlmInboundGroupSession) realm.insertOrUpdate(realmOlmInboundGroupSession)
} }
} }

View File

@ -49,6 +49,8 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034
import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject import javax.inject.Inject
@ -57,7 +59,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer private val normalizer: Normalizer
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Session", dbName = "Session",
schemaVersion = 32L, schemaVersion = 34L,
) { ) {
/** /**
* Forces all RealmSessionStoreMigration instances to be equal. * Forces all RealmSessionStoreMigration instances to be equal.
@ -99,5 +101,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 30) MigrateSessionTo030(realm).perform() if (oldVersion < 30) MigrateSessionTo030(realm).perform()
if (oldVersion < 31) MigrateSessionTo031(realm).perform() if (oldVersion < 31) MigrateSessionTo031(realm).perform()
if (oldVersion < 32) MigrateSessionTo032(realm).perform() if (oldVersion < 32) MigrateSessionTo032(realm).perform()
if (oldVersion < 33) MigrateSessionTo033(realm).perform()
if (oldVersion < 34) MigrateSessionTo034(realm).perform()
} }
} }

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
/**
* Migrating to:
* Live location sharing aggregated summary: adding new field relatedEventIds.
*/
internal class MigrateSessionTo033(realm: DynamicRealm) : RealmMigrator(realm, 33) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("LiveLocationShareAggregatedSummaryEntity")
?.addRealmListField(LiveLocationShareAggregatedSummaryEntityFields.RELATED_EVENT_IDS.`$`, String::class.java)
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
/**
* Migrating to:
* Live location sharing aggregated summary: adding new field startOfLiveTimestampMillis.
*/
internal class MigrateSessionTo034(realm: DynamicRealm) : RealmMigrator(realm, 34) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("LiveLocationShareAggregatedSummaryEntity")
?.addField(LiveLocationShareAggregatedSummaryEntityFields.START_OF_LIVE_TIMESTAMP_MILLIS, Long::class.java)
?.setNullable(LiveLocationShareAggregatedSummaryEntityFields.START_OF_LIVE_TIMESTAMP_MILLIS, true)
}
}

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.database.model.livelocation package org.matrix.android.sdk.internal.database.model.livelocation
import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
@ -29,6 +30,11 @@ internal open class LiveLocationShareAggregatedSummaryEntity(
@PrimaryKey @PrimaryKey
var eventId: String = "", var eventId: String = "",
/**
* List of event ids used to compute the aggregated summary data.
*/
var relatedEventIds: RealmList<String> = RealmList(),
var roomId: String = "", var roomId: String = "",
var userId: String = "", var userId: String = "",
@ -38,6 +44,8 @@ internal open class LiveLocationShareAggregatedSummaryEntity(
*/ */
var isActive: Boolean? = null, var isActive: Boolean? = null,
var startOfLiveTimestampMillis: Long? = null,
var endOfLiveTimestampMillis: Long? = null, var endOfLiveTimestampMillis: Long? = null,
/** /**

View File

@ -23,6 +23,11 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventAnnotationsSummaryEntity> {
return realm.where<EventAnnotationsSummaryEntity>()
.equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId)
}
internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<EventAnnotationsSummaryEntity> { internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<EventAnnotationsSummaryEntity> {
return realm.where<EventAnnotationsSummaryEntity>() return realm.where<EventAnnotationsSummaryEntity>()
.equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId) .equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId)
@ -44,3 +49,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.getOrCreate(realm: Realm, r
return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId) ?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId)
} }
internal fun EventAnnotationsSummaryEntity.Companion.get(realm: Realm, eventId: String): EventAnnotationsSummaryEntity? {
return EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
}

View File

@ -23,6 +23,14 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where(
realm: Realm,
eventId: String,
): RealmQuery<LiveLocationShareAggregatedSummaryEntity> {
return realm.where<LiveLocationShareAggregatedSummaryEntity>()
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
}
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where( internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where(
realm: Realm, realm: Realm,
roomId: String, roomId: String,
@ -72,17 +80,26 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get(
return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst() return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst()
} }
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get(
realm: Realm,
eventId: String,
): LiveLocationShareAggregatedSummaryEntity? {
return LiveLocationShareAggregatedSummaryEntity.where(realm, eventId).findFirst()
}
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveInRoomForUser( internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveInRoomForUser(
realm: Realm, realm: Realm,
roomId: String, roomId: String,
userId: String, userId: String,
ignoredEventId: String, ignoredEventId: String,
startOfLiveTimestampThreshold: Long,
): List<LiveLocationShareAggregatedSummaryEntity> { ): List<LiveLocationShareAggregatedSummaryEntity> {
return LiveLocationShareAggregatedSummaryEntity return LiveLocationShareAggregatedSummaryEntity
.whereRoomId(realm, roomId = roomId) .whereRoomId(realm, roomId = roomId)
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, userId) .equalTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, userId)
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
.notEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, ignoredEventId) .notEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, ignoredEventId)
.lessThan(LiveLocationShareAggregatedSummaryEntityFields.START_OF_LIVE_TIMESTAMP_MILLIS, startOfLiveTimestampThreshold)
.findAll() .findAll()
.toList() .toList()
} }

View File

@ -88,6 +88,7 @@ import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationPro
import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor
import org.matrix.android.sdk.internal.session.room.location.LiveLocationShareRedactionEventProcessor
import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine
@ -321,6 +322,10 @@ internal abstract class SessionModule {
@IntoSet @IntoSet
abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor
@Binds
@IntoSet
abstract fun bindLiveLocationShareRedactionEventProcessor(processor: LiveLocationShareRedactionEventProcessor): EventInsertLiveProcessor
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor

View File

@ -58,11 +58,13 @@ import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVi
import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask
import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask
import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask
import org.matrix.android.sdk.internal.session.room.location.DefaultRedactLiveLocationShareTask
import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask
import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask
import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask
import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask
import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask
import org.matrix.android.sdk.internal.session.room.location.RedactLiveLocationShareTask
import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask
import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask
import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask
@ -339,4 +341,7 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask
@Binds
abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask
} }

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import io.realm.Realm import io.realm.Realm
import io.realm.RealmList
import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
@ -73,16 +74,22 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
eventId = targetEventId eventId = targetEventId
) )
if (!isLive && !event.eventId.isNullOrEmpty()) {
// in this case, the received event is a new state event related to the previous one
addRelatedEventId(event.eventId, aggregatedSummary)
}
// remote event can stay with isLive == true while the local summary is no more active // remote event can stay with isLive == true while the local summary is no more active
val isActive = aggregatedSummary.isActive.orTrue() && isLive val isActive = aggregatedSummary.isActive.orTrue() && isLive
val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) } val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
Timber.d("updating summary of id=$targetEventId with isActive=$isActive and endTimestamp=$endOfLiveTimestampMillis") Timber.d("updating summary of id=$targetEventId with isActive=$isActive and endTimestamp=$endOfLiveTimestampMillis")
aggregatedSummary.startOfLiveTimestampMillis = content.getBestTimestampMillis()
aggregatedSummary.endOfLiveTimestampMillis = endOfLiveTimestampMillis aggregatedSummary.endOfLiveTimestampMillis = endOfLiveTimestampMillis
aggregatedSummary.isActive = isActive aggregatedSummary.isActive = isActive
aggregatedSummary.userId = event.senderId aggregatedSummary.userId = event.senderId
deactivateAllPreviousBeacons(realm, roomId, event.senderId, targetEventId) deactivateAllPreviousBeacons(realm, roomId, event.senderId, targetEventId, content.getBestTimestampMillis() ?: 0)
if (isActive) { if (isActive) {
scheduleDeactivationAfterTimeout(targetEventId, roomId, endOfLiveTimestampMillis) scheduleDeactivationAfterTimeout(targetEventId, roomId, endOfLiveTimestampMillis)
@ -144,6 +151,11 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
roomId = roomId, roomId = roomId,
eventId = relatedEventId eventId = relatedEventId
) )
if (!event.eventId.isNullOrEmpty()) {
addRelatedEventId(event.eventId, aggregatedSummary)
}
val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0 val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0
val currentLocationTimestamp = ContentMapper val currentLocationTimestamp = ContentMapper
.map(aggregatedSummary.lastLocationContent) .map(aggregatedSummary.lastLocationContent)
@ -160,13 +172,31 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
} }
} }
private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) { private fun addRelatedEventId(
eventId: String,
aggregatedSummary: LiveLocationShareAggregatedSummaryEntity
) {
Timber.d("adding related event id $eventId to summary of id ${aggregatedSummary.eventId}")
val updatedEventIds = aggregatedSummary.relatedEventIds.toMutableList().also {
it.add(eventId)
}
aggregatedSummary.relatedEventIds = RealmList(*updatedEventIds.toTypedArray())
}
private fun deactivateAllPreviousBeacons(
realm: Realm,
roomId: String,
userId: String,
currentEventId: String,
currentEventTimestamp: Long
) {
LiveLocationShareAggregatedSummaryEntity LiveLocationShareAggregatedSummaryEntity
.findActiveLiveInRoomForUser( .findActiveLiveInRoomForUser(
realm = realm, realm = realm,
roomId = roomId, roomId = roomId,
userId = userId, userId = userId,
ignoredEventId = currentEventId ignoredEventId = currentEventId,
startOfLiveTimestampThreshold = currentEventTimestamp
) )
.forEach { it.isActive = false } .forEach { it.isActive = false }
} }

View File

@ -42,6 +42,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
private val startLiveLocationShareTask: StartLiveLocationShareTask, private val startLiveLocationShareTask: StartLiveLocationShareTask,
private val stopLiveLocationShareTask: StopLiveLocationShareTask, private val stopLiveLocationShareTask: StopLiveLocationShareTask,
private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask, private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask,
private val redactLiveLocationShareTask: RedactLiveLocationShareTask,
private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper,
) : LocationSharingService { ) : LocationSharingService {
@ -102,6 +103,15 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
return stopLiveLocationShareTask.execute(params) return stopLiveLocationShareTask.execute(params)
} }
override suspend fun redactLiveLocationShare(beaconInfoEventId: String, reason: String?) {
val params = RedactLiveLocationShareTask.Params(
roomId = roomId,
beaconInfoEventId = beaconInfoEventId,
reason = reason
)
return redactLiveLocationShareTask.execute(params)
}
override fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> { override fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) }, { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) },

View File

@ -0,0 +1,65 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.location
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.query.get
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import timber.log.Timber
import javax.inject.Inject
/**
* Listens to the database for the insertion of any redaction event.
* Delete specifically the aggregated summary related to a redacted live location share event.
*/
internal class LiveLocationShareRedactionEventProcessor @Inject constructor() : EventInsertLiveProcessor {
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO
}
override suspend fun process(realm: Realm, event: Event) {
if (event.redacts.isNullOrBlank() || LocalEcho.isLocalEchoId(event.eventId.orEmpty())) {
return
}
val redactedEvent = EventEntity.where(realm, eventId = event.redacts).findFirst()
?: return
if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO) {
val liveSummary = LiveLocationShareAggregatedSummaryEntity.get(realm, eventId = redactedEvent.eventId)
if (liveSummary != null) {
Timber.d("deleting live summary with id: ${liveSummary.eventId}")
liveSummary.deleteFromRealm()
val annotationsSummary = EventAnnotationsSummaryEntity.get(realm, eventId = redactedEvent.eventId)
if (annotationsSummary != null) {
Timber.d("deleting annotation summary with id: ${annotationsSummary.eventId}")
annotationsSummary.deleteFromRealm()
}
}
}
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.location
import io.realm.RealmConfiguration
import org.matrix.android.sdk.internal.database.awaitTransaction
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.query.get
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber
import javax.inject.Inject
internal interface RedactLiveLocationShareTask : Task<RedactLiveLocationShareTask.Params, Unit> {
data class Params(
val roomId: String,
val beaconInfoEventId: String,
val reason: String?
)
}
internal class DefaultRedactLiveLocationShareTask @Inject constructor(
@SessionDatabase private val realmConfiguration: RealmConfiguration,
private val localEchoEventFactory: LocalEchoEventFactory,
private val eventSenderProcessor: EventSenderProcessor,
) : RedactLiveLocationShareTask {
override suspend fun execute(params: RedactLiveLocationShareTask.Params) {
val relatedEventIds = getRelatedEventIdsOfLive(params.beaconInfoEventId)
Timber.d("beacon with id ${params.beaconInfoEventId} has related event ids: ${relatedEventIds.joinToString(", ")}")
postRedactionWithLocalEcho(
eventId = params.beaconInfoEventId,
roomId = params.roomId,
reason = params.reason
)
relatedEventIds.forEach { eventId ->
postRedactionWithLocalEcho(
eventId = eventId,
roomId = params.roomId,
reason = params.reason
)
}
}
private suspend fun getRelatedEventIdsOfLive(beaconInfoEventId: String): List<String> {
return awaitTransaction(realmConfiguration) { realm ->
val aggregatedSummaryEntity = LiveLocationShareAggregatedSummaryEntity.get(
realm = realm,
eventId = beaconInfoEventId
)
aggregatedSummaryEntity?.relatedEventIds?.toList() ?: emptyList()
}
}
private fun postRedactionWithLocalEcho(eventId: String, roomId: String, reason: String?) {
Timber.d("posting redaction for event of id $eventId")
val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, eventId, reason)
localEchoEventFactory.createLocalEcho(redactionEcho)
eventSenderProcessor.postRedaction(redactionEcho, reason)
}
}

View File

@ -74,6 +74,8 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
when (typeToPrune) { when (typeToPrune) {
EventType.ENCRYPTED, EventType.ENCRYPTED,
EventType.MESSAGE, EventType.MESSAGE,
in EventType.STATE_ROOM_BEACON_INFO,
in EventType.BEACON_LOCATION_DATA,
in EventType.POLL_START -> { in EventType.POLL_START -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}") Timber.d("REDACTION for message ${eventToPrune.eventId}")
val unsignedData = EventMapper.map(eventToPrune).unsignedData val unsignedData = EventMapper.map(eventToPrune).unsignedData

View File

@ -22,7 +22,7 @@ import timber.log.Timber
/** /**
* Throws in debug, only log in production. * Throws in debug, only log in production.
* As this method does not always throw, next statement should be a return. * As this method does not always throw, next statement should be a return.
*/ */
internal fun fatalError(message: String) { internal fun fatalError(message: String) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
error(message) error(message)

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldContain
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.UnsignedData import org.matrix.android.sdk.api.session.events.model.UnsignedData
@ -35,6 +36,7 @@ import org.matrix.android.sdk.test.fakes.FakeWorkManagerProvider
import org.matrix.android.sdk.test.fakes.givenEqualTo import org.matrix.android.sdk.test.fakes.givenEqualTo
import org.matrix.android.sdk.test.fakes.givenFindAll import org.matrix.android.sdk.test.fakes.givenFindAll
import org.matrix.android.sdk.test.fakes.givenFindFirst import org.matrix.android.sdk.test.fakes.givenFindFirst
import org.matrix.android.sdk.test.fakes.givenLessThan
import org.matrix.android.sdk.test.fakes.givenNotEqualTo import org.matrix.android.sdk.test.fakes.givenNotEqualTo
private const val A_SESSION_ID = "session_id" private const val A_SESSION_ID = "session_id"
@ -182,6 +184,7 @@ internal class LiveLocationAggregationProcessorTest {
aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID
aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID
aggregatedEntity.isActive shouldBeEqualTo true aggregatedEntity.isActive shouldBeEqualTo true
aggregatedEntity.startOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP
aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS
aggregatedEntity.lastLocationContent shouldBeEqualTo null aggregatedEntity.lastLocationContent shouldBeEqualTo null
previousEntities.forEach { entity -> previousEntities.forEach { entity ->
@ -199,9 +202,10 @@ internal class LiveLocationAggregationProcessorTest {
age = 123, age = 123,
replacesState = AN_EVENT_ID replacesState = AN_EVENT_ID
) )
val stateEventId = "state-event-id"
val event = Event( val event = Event(
senderId = A_SENDER_ID, senderId = A_SENDER_ID,
eventId = "", eventId = stateEventId,
unsignedData = unsignedData unsignedData = unsignedData
) )
val beaconInfo = MessageBeaconInfoContent( val beaconInfo = MessageBeaconInfoContent(
@ -237,6 +241,7 @@ internal class LiveLocationAggregationProcessorTest {
aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID
aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID
aggregatedEntity.isActive shouldBeEqualTo false aggregatedEntity.isActive shouldBeEqualTo false
aggregatedEntity.relatedEventIds shouldContain stateEventId
aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS
aggregatedEntity.lastLocationContent shouldBeEqualTo null aggregatedEntity.lastLocationContent shouldBeEqualTo null
previousEntities.forEach { entity -> previousEntities.forEach { entity ->
@ -324,7 +329,7 @@ internal class LiveLocationAggregationProcessorTest {
val lastBeaconLocationContent = MessageBeaconLocationDataContent( val lastBeaconLocationContent = MessageBeaconLocationDataContent(
unstableTimestampMillis = A_TIMESTAMP unstableTimestampMillis = A_TIMESTAMP
) )
givenLastSummaryQueryReturns( val aggregatedEntity = givenLastSummaryQueryReturns(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
beaconLocationContent = lastBeaconLocationContent beaconLocationContent = lastBeaconLocationContent
@ -340,6 +345,7 @@ internal class LiveLocationAggregationProcessorTest {
) )
result shouldBeEqualTo false result shouldBeEqualTo false
aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID
} }
@Test @Test
@ -353,7 +359,7 @@ internal class LiveLocationAggregationProcessorTest {
val lastBeaconLocationContent = MessageBeaconLocationDataContent( val lastBeaconLocationContent = MessageBeaconLocationDataContent(
unstableTimestampMillis = A_TIMESTAMP - 60_000 unstableTimestampMillis = A_TIMESTAMP - 60_000
) )
val entity = givenLastSummaryQueryReturns( val aggregatedEntity = givenLastSummaryQueryReturns(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
beaconLocationContent = lastBeaconLocationContent beaconLocationContent = lastBeaconLocationContent
@ -369,7 +375,8 @@ internal class LiveLocationAggregationProcessorTest {
) )
result shouldBeEqualTo true result shouldBeEqualTo true
val savedLocationData = ContentMapper.map(entity.lastLocationContent).toModel<MessageBeaconLocationDataContent>() aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID
val savedLocationData = ContentMapper.map(aggregatedEntity.lastLocationContent).toModel<MessageBeaconLocationDataContent>()
savedLocationData?.getBestTimestampMillis() shouldBeEqualTo A_TIMESTAMP savedLocationData?.getBestTimestampMillis() shouldBeEqualTo A_TIMESTAMP
savedLocationData?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI savedLocationData?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI
} }
@ -399,6 +406,7 @@ internal class LiveLocationAggregationProcessorTest {
.givenNotEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID) .givenNotEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID)
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, A_SENDER_ID) .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, A_SENDER_ID)
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
.givenLessThan(LiveLocationShareAggregatedSummaryEntityFields.START_OF_LIVE_TIMESTAMP_MILLIS, A_TIMESTAMP)
.givenFindAll(summaryList) .givenFindAll(summaryList)
return summaryList return summaryList
} }

View File

@ -22,8 +22,10 @@ import androidx.lifecycle.Transformations
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.slot import io.mockk.slot
import io.mockk.unmockkAll import io.mockk.unmockkAll
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -52,6 +54,7 @@ private const val A_LONGITUDE = 40.0
private const val AN_UNCERTAINTY = 5.0 private const val AN_UNCERTAINTY = 5.0
private const val A_TIMEOUT = 15_000L private const val A_TIMEOUT = 15_000L
private const val A_DESCRIPTION = "description" private const val A_DESCRIPTION = "description"
private const val A_REASON = "reason"
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
internal class DefaultLocationSharingServiceTest { internal class DefaultLocationSharingServiceTest {
@ -62,6 +65,7 @@ internal class DefaultLocationSharingServiceTest {
private val startLiveLocationShareTask = mockk<StartLiveLocationShareTask>() private val startLiveLocationShareTask = mockk<StartLiveLocationShareTask>()
private val stopLiveLocationShareTask = mockk<StopLiveLocationShareTask>() private val stopLiveLocationShareTask = mockk<StopLiveLocationShareTask>()
private val checkIfExistingActiveLiveTask = mockk<CheckIfExistingActiveLiveTask>() private val checkIfExistingActiveLiveTask = mockk<CheckIfExistingActiveLiveTask>()
private val redactLiveLocationShareTask = mockk<RedactLiveLocationShareTask>()
private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>() private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>()
private val defaultLocationSharingService = DefaultLocationSharingService( private val defaultLocationSharingService = DefaultLocationSharingService(
@ -72,6 +76,7 @@ internal class DefaultLocationSharingServiceTest {
startLiveLocationShareTask = startLiveLocationShareTask, startLiveLocationShareTask = startLiveLocationShareTask,
stopLiveLocationShareTask = stopLiveLocationShareTask, stopLiveLocationShareTask = stopLiveLocationShareTask,
checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask, checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask,
redactLiveLocationShareTask = redactLiveLocationShareTask,
liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper
) )
@ -209,6 +214,20 @@ internal class DefaultLocationSharingServiceTest {
coVerify { stopLiveLocationShareTask.execute(expectedParams) } coVerify { stopLiveLocationShareTask.execute(expectedParams) }
} }
@Test
fun `live location share can be redacted`() = runTest {
coEvery { redactLiveLocationShareTask.execute(any()) } just runs
defaultLocationSharingService.redactLiveLocationShare(beaconInfoEventId = AN_EVENT_ID, reason = A_REASON)
val expectedParams = RedactLiveLocationShareTask.Params(
roomId = A_ROOM_ID,
beaconInfoEventId = AN_EVENT_ID,
reason = A_REASON
)
coVerify { redactLiveLocationShareTask.execute(expectedParams) }
}
@Test @Test
fun `livedata of live summaries is correctly computed`() { fun `livedata of live summaries is correctly computed`() {
val entity = LiveLocationShareAggregatedSummaryEntity() val entity = LiveLocationShareAggregatedSummaryEntity()

View File

@ -0,0 +1,126 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.location
import io.mockk.unmockkAll
import io.realm.RealmList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor
import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory
import org.matrix.android.sdk.test.fakes.FakeRealm
import org.matrix.android.sdk.test.fakes.FakeRealmConfiguration
import org.matrix.android.sdk.test.fakes.givenEqualTo
import org.matrix.android.sdk.test.fakes.givenFindFirst
private const val A_ROOM_ID = "room-id"
private const val AN_EVENT_ID = "event-id"
private const val AN_EVENT_ID_1 = "event-id-1"
private const val AN_EVENT_ID_2 = "event-id-2"
private const val AN_EVENT_ID_3 = "event-id-3"
private const val A_REASON = "reason"
@ExperimentalCoroutinesApi
class DefaultRedactLiveLocationShareTaskTest {
private val fakeRealmConfiguration = FakeRealmConfiguration()
private val fakeLocalEchoEventFactory = FakeLocalEchoEventFactory()
private val fakeEventSenderProcessor = FakeEventSenderProcessor()
private val fakeRealm = FakeRealm()
private val defaultRedactLiveLocationShareTask = DefaultRedactLiveLocationShareTask(
realmConfiguration = fakeRealmConfiguration.instance,
localEchoEventFactory = fakeLocalEchoEventFactory.instance,
eventSenderProcessor = fakeEventSenderProcessor
)
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given parameters when redacting then post redact events and related and creates redact local echos`() = runTest {
val params = createParams()
val relatedEventIds = listOf(AN_EVENT_ID_1, AN_EVENT_ID_2, AN_EVENT_ID_3)
val aggregatedSummaryEntity = createSummary(relatedEventIds)
givenSummaryForId(AN_EVENT_ID, aggregatedSummaryEntity)
fakeRealmConfiguration.givenAwaitTransaction<List<String>>(fakeRealm.instance)
val redactEvents = givenCreateRedactEventWithLocalEcho(relatedEventIds + AN_EVENT_ID)
givenPostRedaction(redactEvents)
defaultRedactLiveLocationShareTask.execute(params)
verifyCreateRedactEventForEventIds(relatedEventIds + AN_EVENT_ID)
verifyCreateLocalEchoForEvents(redactEvents)
}
private fun createParams() = RedactLiveLocationShareTask.Params(
roomId = A_ROOM_ID,
beaconInfoEventId = AN_EVENT_ID,
reason = A_REASON
)
private fun createSummary(relatedEventIds: List<String>): LiveLocationShareAggregatedSummaryEntity {
return LiveLocationShareAggregatedSummaryEntity(
eventId = AN_EVENT_ID,
relatedEventIds = RealmList(*relatedEventIds.toTypedArray()),
)
}
private fun givenSummaryForId(eventId: String, aggregatedSummaryEntity: LiveLocationShareAggregatedSummaryEntity) {
fakeRealm.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
.givenFindFirst(aggregatedSummaryEntity)
}
private fun givenCreateRedactEventWithLocalEcho(eventIds: List<String>): List<Event> {
return eventIds.map { eventId ->
fakeLocalEchoEventFactory.givenCreateRedactEvent(
eventId = eventId,
withLocalEcho = true
)
}
}
private fun givenPostRedaction(redactEvents: List<Event>) {
redactEvents.forEach {
fakeEventSenderProcessor.givenPostRedaction(event = it, reason = A_REASON)
}
}
private fun verifyCreateRedactEventForEventIds(eventIds: List<String>) {
eventIds.forEach { eventId ->
fakeLocalEchoEventFactory.verifyCreateRedactEvent(
roomId = A_ROOM_ID,
eventId = eventId,
reason = A_REASON
)
}
}
private fun verifyCreateLocalEchoForEvents(events: List<Event>) {
events.forEach { redactionEvent ->
fakeLocalEchoEventFactory.verifyCreateLocalEcho(redactionEvent)
}
}
}

View File

@ -0,0 +1,106 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.location
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
import org.matrix.android.sdk.test.fakes.FakeRealm
import org.matrix.android.sdk.test.fakes.givenDelete
import org.matrix.android.sdk.test.fakes.givenEqualTo
import org.matrix.android.sdk.test.fakes.givenFindFirst
private const val AN_EVENT_ID = "event-id"
private const val A_REDACTED_EVENT_ID = "redacted-event-id"
@ExperimentalCoroutinesApi
class LiveLocationShareRedactionEventProcessorTest {
private val liveLocationShareRedactionEventProcessor = LiveLocationShareRedactionEventProcessor()
private val fakeRealm = FakeRealm()
@Test
fun `given an event when checking if it should be processed then only event of type REDACTED is processed`() {
val eventId = AN_EVENT_ID
val eventType = EventType.REDACTION
val insertType = EventInsertType.INCREMENTAL_SYNC
val result = liveLocationShareRedactionEventProcessor.shouldProcess(
eventId = eventId,
eventType = eventType,
insertType = insertType
)
result shouldBe true
}
@Test
fun `given an event when checking if it should be processed then local echo is not processed`() {
val eventId = AN_EVENT_ID
val eventType = EventType.REDACTION
val insertType = EventInsertType.LOCAL_ECHO
val result = liveLocationShareRedactionEventProcessor.shouldProcess(
eventId = eventId,
eventType = eventType,
insertType = insertType
)
result shouldBe false
}
@Test
fun `given a redacted live location share event when processing it then related summaries are deleted from database`() = runTest {
val event = Event(eventId = AN_EVENT_ID, redacts = A_REDACTED_EVENT_ID)
val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.first())
fakeRealm.givenWhere<EventEntity>()
.givenEqualTo(EventEntityFields.EVENT_ID, A_REDACTED_EVENT_ID)
.givenFindFirst(redactedEventEntity)
val liveSummary = mockk<LiveLocationShareAggregatedSummaryEntity>()
every { liveSummary.eventId } returns A_REDACTED_EVENT_ID
liveSummary.givenDelete()
fakeRealm.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, A_REDACTED_EVENT_ID)
.givenFindFirst(liveSummary)
val annotationsSummary = mockk<EventAnnotationsSummaryEntity>()
every { annotationsSummary.eventId } returns A_REDACTED_EVENT_ID
annotationsSummary.givenDelete()
fakeRealm.givenWhere<EventAnnotationsSummaryEntity>()
.givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_REDACTED_EVENT_ID)
.givenFindFirst(annotationsSummary)
liveLocationShareRedactionEventProcessor.process(fakeRealm.instance, event = event)
verify {
liveSummary.deleteFromRealm()
annotationsSummary.deleteFromRealm()
}
}
}

View File

@ -27,4 +27,8 @@ internal class FakeEventSenderProcessor : EventSenderProcessor by mockk() {
fun givenPostEventReturns(event: Event, cancelable: Cancelable) { fun givenPostEventReturns(event: Event, cancelable: Cancelable) {
every { postEvent(event) } returns cancelable every { postEvent(event) } returns cancelable
} }
fun givenPostRedaction(event: Event, reason: String?) {
every { postRedaction(event, reason) } returns mockk()
}
} }

View File

@ -46,24 +46,6 @@ internal class FakeLocalEchoEventFactory {
return event return event
} }
fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event {
val event = Event()
every {
instance.createLiveLocationEvent(
beaconInfoEventId = any(),
roomId = any(),
latitude = any(),
longitude = any(),
uncertainty = any()
)
} returns event
if (withLocalEcho) {
every { instance.createLocalEcho(event) } just runs
}
return event
}
fun verifyCreateStaticLocationEvent( fun verifyCreateStaticLocationEvent(
roomId: String, roomId: String,
latitude: Double, latitude: Double,
@ -82,6 +64,24 @@ internal class FakeLocalEchoEventFactory {
} }
} }
fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event {
val event = Event()
every {
instance.createLiveLocationEvent(
beaconInfoEventId = any(),
roomId = any(),
latitude = any(),
longitude = any(),
uncertainty = any()
)
} returns event
if (withLocalEcho) {
every { instance.createLocalEcho(event) } just runs
}
return event
}
fun verifyCreateLiveLocationEvent( fun verifyCreateLiveLocationEvent(
roomId: String, roomId: String,
beaconInfoEventId: String, beaconInfoEventId: String,
@ -100,6 +100,36 @@ internal class FakeLocalEchoEventFactory {
} }
} }
fun givenCreateRedactEvent(eventId: String, withLocalEcho: Boolean): Event {
val event = Event()
every {
instance.createRedactEvent(
roomId = any(),
eventId = eventId,
reason = any()
)
} returns event
if (withLocalEcho) {
every { instance.createLocalEcho(event) } just runs
}
return event
}
fun verifyCreateRedactEvent(
roomId: String,
eventId: String,
reason: String?
) {
verify {
instance.createRedactEvent(
roomId = roomId,
eventId = eventId,
reason = reason
)
}
}
fun verifyCreateLocalEcho(event: Event) { fun verifyCreateLocalEcho(event: Event) {
verify { instance.createLocalEcho(event) } verify { instance.createLocalEcho(event) }
} }

View File

@ -18,10 +18,13 @@ package org.matrix.android.sdk.test.fakes
import io.mockk.MockKVerificationScope import io.mockk.MockKVerificationScope
import io.mockk.every import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify import io.mockk.verify
import io.realm.Realm import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import io.realm.RealmObject
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.kotlin.where import io.realm.kotlin.where
@ -97,3 +100,18 @@ inline fun <reified T : RealmModel> RealmQuery<T>.givenIsNotNull(
every { isNotNull(fieldName) } returns this every { isNotNull(fieldName) } returns this
return this return this
} }
inline fun <reified T : RealmModel> RealmQuery<T>.givenLessThan(
fieldName: String,
value: Long
): RealmQuery<T> {
every { lessThan(fieldName, value) } returns this
return this
}
/**
* Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked.
*/
fun RealmObject.givenDelete() {
every { deleteFromRealm() } just runs
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.test.fakes
import io.mockk.coEvery
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import io.realm.Realm
import io.realm.RealmConfiguration
import org.matrix.android.sdk.internal.database.awaitTransaction
internal class FakeRealmConfiguration {
init {
mockkStatic("org.matrix.android.sdk.internal.database.AsyncTransactionKt")
}
val instance = mockk<RealmConfiguration>()
fun <T> givenAwaitTransaction(realm: Realm) {
val transaction = slot<suspend (Realm) -> T>()
coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers {
secondArg<suspend (Realm) -> T>().invoke(realm)
}
}
}

View File

@ -437,7 +437,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0' implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber // Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.51' implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.52'
// FlowBinding // FlowBinding
implementation libs.github.flowBinding implementation libs.github.flowBinding

View File

@ -21,11 +21,11 @@ import im.vector.app.features.onboarding.AuthenticationDescription
fun AuthenticationDescription.AuthenticationType.toAnalyticsType() = when (this) { fun AuthenticationDescription.AuthenticationType.toAnalyticsType() = when (this) {
AuthenticationDescription.AuthenticationType.Password -> Signup.AuthenticationType.Password AuthenticationDescription.AuthenticationType.Password -> Signup.AuthenticationType.Password
AuthenticationDescription.AuthenticationType.Apple -> Signup.AuthenticationType.Apple AuthenticationDescription.AuthenticationType.Apple -> Signup.AuthenticationType.Apple
AuthenticationDescription.AuthenticationType.Facebook -> Signup.AuthenticationType.Facebook AuthenticationDescription.AuthenticationType.Facebook -> Signup.AuthenticationType.Facebook
AuthenticationDescription.AuthenticationType.GitHub -> Signup.AuthenticationType.GitHub AuthenticationDescription.AuthenticationType.GitHub -> Signup.AuthenticationType.GitHub
AuthenticationDescription.AuthenticationType.GitLab -> Signup.AuthenticationType.GitLab AuthenticationDescription.AuthenticationType.GitLab -> Signup.AuthenticationType.GitLab
AuthenticationDescription.AuthenticationType.Google -> Signup.AuthenticationType.Google AuthenticationDescription.AuthenticationType.Google -> Signup.AuthenticationType.Google
AuthenticationDescription.AuthenticationType.SSO -> Signup.AuthenticationType.SSO AuthenticationDescription.AuthenticationType.SSO -> Signup.AuthenticationType.SSO
AuthenticationDescription.AuthenticationType.Other -> Signup.AuthenticationType.Other AuthenticationDescription.AuthenticationType.Other -> Signup.AuthenticationType.Other
} }

View File

@ -70,7 +70,7 @@ class AvatarRenderer @Inject constructor(
render( render(
GlideApp.with(imageView), GlideApp.with(imageView),
matrixItem, matrixItem,
DrawableImageViewTarget(imageView) DrawableImageViewTarget(imageView),
) )
} }
@ -103,7 +103,7 @@ class AvatarRenderer @Inject constructor(
render( render(
glideRequests, glideRequests,
matrixItem, matrixItem,
DrawableImageViewTarget(imageView) DrawableImageViewTarget(imageView),
) )
} }
@ -123,7 +123,7 @@ class AvatarRenderer @Inject constructor(
val matrixItem = MatrixItem.UserItem( val matrixItem = MatrixItem.UserItem(
// Need an id starting with @ // Need an id starting with @
id = "@${mappedContact.displayName}", id = "@${mappedContact.displayName}",
displayName = mappedContact.displayName displayName = mappedContact.displayName,
) )
val placeholder = getPlaceholderDrawable(matrixItem) val placeholder = getPlaceholderDrawable(matrixItem)
@ -140,7 +140,7 @@ class AvatarRenderer @Inject constructor(
val matrixItem = MatrixItem.UserItem( val matrixItem = MatrixItem.UserItem(
// Need an id starting with @ // Need an id starting with @
id = profileInfo.matrixId, id = profileInfo.matrixId,
displayName = profileInfo.displayName displayName = profileInfo.displayName,
) )
val placeholder = getPlaceholderDrawable(matrixItem) val placeholder = getPlaceholderDrawable(matrixItem)
@ -215,7 +215,7 @@ class AvatarRenderer @Inject constructor(
.bold() .bold()
.endConfig() .endConfig()
.buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor) .buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor)
.toBitmap(width = iconSize, height = iconSize) .toBitmap(width = iconSize, height = iconSize),
) )
} }
} }
@ -231,7 +231,7 @@ class AvatarRenderer @Inject constructor(
addPlaceholder: Boolean addPlaceholder: Boolean
) { ) {
val transformations = mutableListOf<Transformation<Bitmap>>( val transformations = mutableListOf<Transformation<Bitmap>>(
BlurTransformation(20, sampling) BlurTransformation(20, sampling),
) )
if (colorFilter != null) { if (colorFilter != null) {
transformations.add(ColorFilterTransformation(colorFilter)) transformations.add(ColorFilterTransformation(colorFilter))

View File

@ -219,7 +219,7 @@ class HomeActivity :
is HomeActivitySharedAction.ShowSpaceSettings -> showSpaceSettings(sharedAction.spaceId) is HomeActivitySharedAction.ShowSpaceSettings -> showSpaceSettings(sharedAction.spaceId)
is HomeActivitySharedAction.OpenSpaceInvite -> openSpaceInvite(sharedAction.spaceId) is HomeActivitySharedAction.OpenSpaceInvite -> openSpaceInvite(sharedAction.spaceId)
HomeActivitySharedAction.SendSpaceFeedBack -> bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK) HomeActivitySharedAction.SendSpaceFeedBack -> bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK)
HomeActivitySharedAction.OnCloseSpace -> onCloseSpace() HomeActivitySharedAction.OnCloseSpace -> onCloseSpace()
} }
} }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)

View File

@ -84,6 +84,4 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
object StopChatEffects : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents()
object RoomReplacementStarted : RoomDetailViewEvents() object RoomReplacementStarted : RoomDetailViewEvents()
data class ChangeLocationIndicator(val isVisible: Boolean) : RoomDetailViewEvents()
} }

View File

@ -75,7 +75,8 @@ data class RoomDetailViewState(
val switchToParentSpace: Boolean = false, val switchToParentSpace: Boolean = false,
val rootThreadEventId: String? = null, val rootThreadEventId: String? = null,
val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState(), val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState(),
val typingUsers: List<SenderInfo>? = null val typingUsers: List<SenderInfo>? = null,
val isSharingLiveLocation: Boolean = false,
) : MavericksState { ) : MavericksState {
constructor(args: TimelineArgs) : this( constructor(args: TimelineArgs) : this(

View File

@ -498,7 +498,6 @@ class TimelineFragment @Inject constructor(
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
is RoomDetailViewEvents.ChangeLocationIndicator -> handleChangeLocationIndicator(it)
} }
} }
@ -663,10 +662,6 @@ class TimelineFragment @Inject constructor(
) )
} }
private fun handleChangeLocationIndicator(event: RoomDetailViewEvents.ChangeLocationIndicator) {
views.locationLiveStatusIndicator.isVisible = event.isVisible
}
private fun displayErrorMessage(error: RoomDetailViewEvents.Failure) { private fun displayErrorMessage(error: RoomDetailViewEvents.Failure) {
if (error.showInDialog) displayErrorDialog(error.throwable) else showErrorInSnackbar(error.throwable) if (error.showInDialog) displayErrorDialog(error.throwable) else showErrorInSnackbar(error.throwable)
} }
@ -1686,6 +1681,11 @@ class TimelineFragment @Inject constructor(
} else if (mainState.asyncInviter.complete) { } else if (mainState.asyncInviter.complete) {
vectorBaseActivity.finish() vectorBaseActivity.finish()
} }
updateLiveLocationIndicator(mainState.isSharingLiveLocation)
}
private fun updateLiveLocationIndicator(isSharingLiveLocation: Boolean) {
views.locationLiveStatusIndicator.isVisible = isSharingLiveLocation
} }
private fun FragmentTimelineBinding.hideComposerViews() { private fun FragmentTimelineBinding.hideComposerViews() {
@ -1706,7 +1706,7 @@ class TimelineFragment @Inject constructor(
private fun renderToolbar(roomSummary: RoomSummary?) { private fun renderToolbar(roomSummary: RoomSummary?) {
when { when {
isLocalRoom() -> { isLocalRoom() -> {
views.includeRoomToolbar.roomToolbarContentView.isVisible = false views.includeRoomToolbar.roomToolbarContentView.isVisible = false
views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
setupToolbar(views.roomToolbar) setupToolbar(views.roomToolbar)
@ -1724,7 +1724,7 @@ class TimelineFragment @Inject constructor(
} }
views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title) views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title)
} }
else -> { else -> {
views.includeRoomToolbar.roomToolbarContentView.isVisible = true views.includeRoomToolbar.roomToolbarContentView.isVisible = true
views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
if (roomSummary == null) { if (roomSummary == null) {

View File

@ -48,6 +48,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.createdirect.DirectRoomHelper
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
@ -105,6 +106,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.isLiveLocation
import org.matrix.android.sdk.api.session.sync.SyncRequestState import org.matrix.android.sdk.api.session.sync.SyncRequestState
import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
@ -135,6 +137,7 @@ class TimelineViewModel @AssistedInject constructor(
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
private val locationSharingServiceConnection: LocationSharingServiceConnection, private val locationSharingServiceConnection: LocationSharingServiceConnection,
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase, private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
private val redactLiveLocationShareEventUseCase: RedactLiveLocationShareEventUseCase,
timelineFactory: TimelineFactory, timelineFactory: TimelineFactory,
spaceStateHandler: SpaceStateHandler, spaceStateHandler: SpaceStateHandler,
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
@ -770,7 +773,13 @@ class TimelineViewModel @AssistedInject constructor(
private fun handleRedactEvent(action: RoomDetailAction.RedactAction) { private fun handleRedactEvent(action: RoomDetailAction.RedactAction) {
val event = room.getTimelineEvent(action.targetEventId) ?: return val event = room.getTimelineEvent(action.targetEventId) ?: return
room.sendService().redactEvent(event.root, action.reason) if (event.isLiveLocation()) {
viewModelScope.launch {
redactLiveLocationShareEventUseCase.execute(event.root, room, action.reason)
}
} else {
room.sendService().redactEvent(event.root, action.reason)
}
} }
private fun handleUndoReact(action: RoomDetailAction.UndoReaction) { private fun handleUndoReact(action: RoomDetailAction.UndoReaction) {
@ -1294,12 +1303,12 @@ class TimelineViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds)) _viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds))
} }
override fun onLocationServiceRunning() { override fun onLocationServiceRunning(roomIds: Set<String>) {
_viewEvents.post(RoomDetailViewEvents.ChangeLocationIndicator(isVisible = true)) setState { copy(isSharingLiveLocation = roomId in roomIds) }
} }
override fun onLocationServiceStopped() { override fun onLocationServiceStopped() {
_viewEvents.post(RoomDetailViewEvents.ChangeLocationIndicator(isVisible = false)) setState { copy(isSharingLiveLocation = false) }
// Bind again in case user decides to share live location without leaving the room // Bind again in case user decides to share live location without leaving the room
locationSharingServiceConnection.bind(this) locationSharingServiceConnection.bind(this)
} }

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.location
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.Room
import javax.inject.Inject
class RedactLiveLocationShareEventUseCase @Inject constructor() {
suspend fun execute(event: Event, room: Room, reason: String?) {
event.eventId
?.takeUnless { it.isEmpty() }
?.let { room.locationSharingService().redactLiveLocationShare(it, reason) }
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.timeline.action
import im.vector.app.core.di.ActiveSessionHolder
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
class CheckIfCanRedactEventUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder
) {
fun execute(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
// Only some event types are supported for the moment
val canRedactEventTypes = listOf(EventType.MESSAGE, EventType.STICKER) +
EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
return event.root.getClearType() in canRedactEventTypes &&
// Message sent by the current user can always be redacted, else check permission for messages sent by other users
(event.root.senderId == activeSessionHolder.getActiveSession().myUserId || actionPermissions.canRedact)
}
}

View File

@ -82,6 +82,7 @@ class MessageActionsViewModel @AssistedInject constructor(
private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val checkIfCanReplyEventUseCase: CheckIfCanReplyEventUseCase, private val checkIfCanReplyEventUseCase: CheckIfCanReplyEventUseCase,
private val checkIfCanRedactEventUseCase: CheckIfCanRedactEventUseCase,
) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) { ) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) {
private val informationData = initialState.informationData private val informationData = initialState.informationData
@ -518,12 +519,7 @@ class MessageActionsViewModel @AssistedInject constructor(
} }
private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment return checkIfCanRedactEventUseCase.execute(event, actionPermissions)
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false
// Message sent by the current user can always be redacted
if (event.root.senderId == session.myUserId) return true
// Check permission for messages sent by other users
return actionPermissions.canRedact
} }
private fun canRetry(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { private fun canRetry(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {

View File

@ -63,10 +63,10 @@ class EncryptionItemFactory @Inject constructor(
isDirect && RoomLocalEcho.isLocalEchoId(event.root.roomId.orEmpty()) -> { isDirect && RoomLocalEcho.isLocalEchoId(event.root.roomId.orEmpty()) -> {
R.string.direct_room_encryption_enabled_tile_description_future R.string.direct_room_encryption_enabled_tile_description_future
} }
isDirect -> { isDirect -> {
R.string.direct_room_encryption_enabled_tile_description R.string.direct_room_encryption_enabled_tile_description
} }
else -> { else -> {
R.string.encryption_enabled_tile_description R.string.encryption_enabled_tile_description
} }
} }

View File

@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
@ -35,6 +34,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -53,6 +53,7 @@ class MergedHeaderItemFactory @Inject constructor(
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper
) { ) {
private val mergeableEventTypes = listOf(EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_SERVER_ACL)
private val collapsedEventIds = linkedSetOf<Long>() private val collapsedEventIds = linkedSetOf<Long>()
private val mergeItemCollapseStates = HashMap<Long, Boolean>() private val mergeItemCollapseStates = HashMap<Long, Boolean>()
@ -78,19 +79,65 @@ class MergedHeaderItemFactory @Inject constructor(
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
requestModelBuild: () -> Unit requestModelBuild: () -> Unit
): BasedMergedItem<*>? { ): BasedMergedItem<*>? {
return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE && return when {
event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)) { isStartOfRoomCreationSummary(event, nextEvent) ->
// It's the first item before room.create buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
// Collapse all room configuration events isStartOfSameTypeEventsSummary(event, nextEvent, addDaySeparator) ->
buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) buildSameTypeEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
} else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { isStartOfRedactedEventsSummary(event, items, currentPosition, addDaySeparator) ->
null buildRedactedEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
} else { else -> null
buildMembershipEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
} }
} }
private fun buildMembershipEventsMergedSummary( /**
* @param event the main timeline event
* @param nextEvent is an older event than event
*/
private fun isStartOfRoomCreationSummary(
event: TimelineEvent,
nextEvent: TimelineEvent?,
): Boolean {
// It's the first item before room.create
// Collapse all room configuration events
return nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)
}
/**
* @param event the main timeline event
* @param nextEvent is an older event than event
* @param addDaySeparator true to add a day separator
*/
private fun isStartOfSameTypeEventsSummary(
event: TimelineEvent,
nextEvent: TimelineEvent?,
addDaySeparator: Boolean,
): Boolean {
return event.root.getClearType() in mergeableEventTypes &&
(nextEvent?.root?.getClearType() != event.root.getClearType() || addDaySeparator)
}
/**
* @param event the main timeline event
* @param items all known items, sorted from newer event to oldest event
* @param currentPosition the current position
* @param addDaySeparator true to add a day separator
*/
private fun isStartOfRedactedEventsSummary(
event: TimelineEvent,
items: List<TimelineEvent>,
currentPosition: Int,
addDaySeparator: Boolean,
): Boolean {
val nextNonRedactionEvent = items
.subList(fromIndex = currentPosition + 1, toIndex = items.size)
.find { it.root.getClearType() != EventType.REDACTION }
return event.root.isRedacted() &&
(!nextNonRedactionEvent?.root?.isRedacted().orFalse() || addDaySeparator)
}
private fun buildSameTypeEventsMergedSummary(
currentPosition: Int, currentPosition: Int,
items: List<TimelineEvent>, items: List<TimelineEvent>,
partialState: TimelineEventController.PartialState, partialState: TimelineEventController.PartialState,
@ -102,11 +149,42 @@ class MergedHeaderItemFactory @Inject constructor(
val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents( val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(
items, items,
currentPosition, currentPosition,
2, MIN_NUMBER_OF_MERGED_EVENTS,
eventIdToHighlight, eventIdToHighlight,
partialState.rootThreadEventId, partialState.rootThreadEventId,
partialState.isFromThreadTimeline() partialState.isFromThreadTimeline()
) )
return buildSimilarEventsMergedSummary(mergedEvents, partialState, event, eventIdToHighlight, requestModelBuild, callback)
}
private fun buildRedactedEventsMergedSummary(
currentPosition: Int,
items: List<TimelineEvent>,
partialState: TimelineEventController.PartialState,
event: TimelineEvent,
eventIdToHighlight: String?,
requestModelBuild: () -> Unit,
callback: TimelineEventController.Callback?
): MergedSimilarEventsItem_? {
val mergedEvents = timelineEventVisibilityHelper.prevRedactedEvents(
items,
currentPosition,
MIN_NUMBER_OF_MERGED_EVENTS,
eventIdToHighlight,
partialState.rootThreadEventId,
partialState.isFromThreadTimeline()
)
return buildSimilarEventsMergedSummary(mergedEvents, partialState, event, eventIdToHighlight, requestModelBuild, callback)
}
private fun buildSimilarEventsMergedSummary(
mergedEvents: List<TimelineEvent>,
partialState: TimelineEventController.PartialState,
event: TimelineEvent,
eventIdToHighlight: String?,
requestModelBuild: () -> Unit,
callback: TimelineEventController.Callback?
): MergedSimilarEventsItem_? {
return if (mergedEvents.isEmpty()) { return if (mergedEvents.isEmpty()) {
null null
} else { } else {
@ -127,7 +205,7 @@ class MergedHeaderItemFactory @Inject constructor(
) )
mergedData.add(data) mergedData.add(data)
} }
val mergedEventIds = mergedEvents.map { it.localId } val mergedEventIds = mergedEvents.map { it.localId }.toSet()
// We try to find if one of the item id were used as mergeItemCollapseStates key // We try to find if one of the item id were used as mergeItemCollapseStates key
// => handle case where paginating from mergeable events and we get more // => handle case where paginating from mergeable events and we get more
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
@ -140,12 +218,7 @@ class MergedHeaderItemFactory @Inject constructor(
collapsedEventIds.removeAll(mergedEventIds) collapsedEventIds.removeAll(mergedEventIds)
} }
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
val summaryTitleResId = when (event.root.getClearType()) { getSummaryTitleResId(event.root)?.let { summaryTitle ->
EventType.STATE_ROOM_MEMBER -> R.plurals.membership_changes
EventType.STATE_ROOM_SERVER_ACL -> R.plurals.notice_room_server_acl_changes
else -> null
}
summaryTitleResId?.let { summaryTitle ->
val attributes = MergedSimilarEventsItem.Attributes( val attributes = MergedSimilarEventsItem.Attributes(
summaryTitleResId = summaryTitle, summaryTitleResId = summaryTitle,
isCollapsed = isCollapsed, isCollapsed = isCollapsed,
@ -168,6 +241,16 @@ class MergedHeaderItemFactory @Inject constructor(
} }
} }
private fun getSummaryTitleResId(event: Event): Int? {
val type = event.getClearType()
return when {
type == EventType.STATE_ROOM_MEMBER -> R.plurals.membership_changes
type == EventType.STATE_ROOM_SERVER_ACL -> R.plurals.notice_room_server_acl_changes
event.isRedacted() -> R.plurals.room_removed_messages
else -> null
}
}
private fun buildRoomCreationMergedSummary( private fun buildRoomCreationMergedSummary(
currentPosition: Int, currentPosition: Int,
items: List<TimelineEvent>, items: List<TimelineEvent>,
@ -191,7 +274,7 @@ class MergedHeaderItemFactory @Inject constructor(
tmpPos-- tmpPos--
prevEvent = items.getOrNull(tmpPos) prevEvent = items.getOrNull(tmpPos)
} }
return if (mergedEvents.size > 2) { return if (mergedEvents.size > MIN_NUMBER_OF_MERGED_EVENTS) {
var highlighted = false var highlighted = false
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size) val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
mergedEvents.reversed() mergedEvents.reversed()
@ -264,4 +347,8 @@ class MergedHeaderItemFactory @Inject constructor(
fun isCollapsed(localId: Long): Boolean { fun isCollapsed(localId: Long): Boolean {
return collapsedEventIds.contains(localId) return collapsedEventIds.contains(localId)
} }
companion object {
private const val MIN_NUMBER_OF_MERGED_EVENTS = 2
}
} }

View File

@ -113,8 +113,14 @@ class TimelineItemFactory @Inject constructor(
EventType.CALL_NEGOTIATE, EventType.CALL_NEGOTIATE,
EventType.REACTION, EventType.REACTION,
in EventType.POLL_RESPONSE, in EventType.POLL_RESPONSE,
in EventType.POLL_END, in EventType.POLL_END -> noticeItemFactory.create(params)
in EventType.BEACON_LOCATION_DATA -> noticeItemFactory.create(params) in EventType.BEACON_LOCATION_DATA -> {
if (event.root.isRedacted()) {
messageItemFactory.create(params)
} else {
noticeItemFactory.create(params)
}
}
// Calls // Calls
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,

View File

@ -51,12 +51,7 @@ object TimelineDisplayableEvents {
EventType.STATE_ROOM_JOIN_RULES, EventType.STATE_ROOM_JOIN_RULES,
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_CANCEL,
) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA
}
fun TimelineEvent.canBeMerged(): Boolean {
return root.getClearType() == EventType.STATE_ROOM_MEMBER ||
root.getClearType() == EventType.STATE_ROOM_SERVER_ACL
} }
fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean { fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {

View File

@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.core.resources.UserPreferencesProvider
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.getRelationContent
@ -30,25 +31,38 @@ import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject import javax.inject.Inject
class TimelineEventVisibilityHelper @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) { class TimelineEventVisibilityHelper @Inject constructor(
private val userPreferencesProvider: UserPreferencesProvider,
) {
private interface PredicateToStopSearch {
/**
* Indicate whether a search on events should stop by comparing 2 given successive events.
* @param oldEvent the oldest event between the 2 events to compare
* @param newEvent the more recent event between the 2 events to compare
*/
fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean
}
/** /**
* @param timelineEvents the events to search in * @param timelineEvents the events to search in, sorted from oldest event to newer event
* @param index the index to start computing (inclusive) * @param index the index to start computing (inclusive)
* @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list
* @param eventIdToHighlight used to compute visibility * @param eventIdToHighlight used to compute visibility
* @param rootThreadEventId the root thread event id if in a thread timeline * @param rootThreadEventId the root thread event id if in a thread timeline
* @param isFromThreadTimeline true if the timeline is a thread * @param isFromThreadTimeline true if the timeline is a thread
* @param predicateToStop events are taken until this condition is met
* *
* @return a list of timeline events which have sequentially the same type following the next direction. * @return a list of timeline events which meet sequentially the same criteria following the next direction.
*/ */
private fun nextSameTypeEvents( private fun nextEventsUntil(
timelineEvents: List<TimelineEvent>, timelineEvents: List<TimelineEvent>,
index: Int, index: Int,
minSize: Int, minSize: Int,
eventIdToHighlight: String?, eventIdToHighlight: String?,
rootThreadEventId: String?, rootThreadEventId: String?,
isFromThreadTimeline: Boolean isFromThreadTimeline: Boolean,
predicateToStop: PredicateToStopSearch
): List<TimelineEvent> { ): List<TimelineEvent> {
if (index >= timelineEvents.size - 1) { if (index >= timelineEvents.size - 1) {
return emptyList() return emptyList()
@ -65,13 +79,15 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
} else { } else {
nextSubList.subList(0, indexOfNextDay) nextSubList.subList(0, indexOfNextDay)
} }
val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.getClearType() != timelineEvent.root.getClearType() } val indexOfFirstDifferentEvent = nextSameDayEvents.indexOfFirst {
val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) { predicateToStop.shouldStopSearch(oldEvent = timelineEvent.root, newEvent = it.root)
}
val similarEvents = if (indexOfFirstDifferentEvent == -1) {
nextSameDayEvents nextSameDayEvents
} else { } else {
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType) nextSameDayEvents.subList(0, indexOfFirstDifferentEvent)
} }
val filteredSameTypeEvents = sameTypeEvents.filter { val filteredSimilarEvents = similarEvents.filter {
shouldShowEvent( shouldShowEvent(
timelineEvent = it, timelineEvent = it,
highlightedEventId = eventIdToHighlight, highlightedEventId = eventIdToHighlight,
@ -79,14 +95,11 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
rootThreadEventId = rootThreadEventId rootThreadEventId = rootThreadEventId
) )
} }
if (filteredSameTypeEvents.size < minSize) { return if (filteredSimilarEvents.size < minSize) emptyList() else filteredSimilarEvents
return emptyList()
}
return filteredSameTypeEvents
} }
/** /**
* @param timelineEvents the events to search in * @param timelineEvents the events to search in, sorted from newer event to oldest event
* @param index the index to start computing (inclusive) * @param index the index to start computing (inclusive)
* @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list
* @param eventIdToHighlight used to compute visibility * @param eventIdToHighlight used to compute visibility
@ -107,7 +120,44 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
return prevSub return prevSub
.reversed() .reversed()
.let { .let {
nextSameTypeEvents(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline) nextEventsUntil(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline, object : PredicateToStopSearch {
override fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean {
return oldEvent.getClearType() != newEvent.getClearType()
}
})
}
}
/**
* @param timelineEvents the events to search in, sorted from newer event to oldest event
* @param index the index to start computing (inclusive)
* @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list
* @param eventIdToHighlight used to compute visibility
* @param rootThreadEventId the root thread eventId
* @param isFromThreadTimeline true if the timeline is a thread
*
* @return a list of timeline events which are all redacted following the prev direction.
*/
fun prevRedactedEvents(
timelineEvents: List<TimelineEvent>,
index: Int,
minSize: Int,
eventIdToHighlight: String?,
rootThreadEventId: String?,
isFromThreadTimeline: Boolean
): List<TimelineEvent> {
val prevSub = timelineEvents
.subList(0, index + 1)
// Ensure to not take the REDACTION events into account
.filter { it.root.getClearType() != EventType.REDACTION }
return prevSub
.reversed()
.let {
nextEventsUntil(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline, object : PredicateToStopSearch {
override fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean {
return oldEvent.isRedacted() && !newEvent.isRedacted()
}
})
} }
} }
@ -191,6 +241,10 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
} else root.eventId != rootThreadEventId } else root.eventId != rootThreadEventId
} }
if (root.getClearType() in EventType.BEACON_LOCATION_DATA) {
return !root.isRedacted()
}
return false return false
} }

View File

@ -84,8 +84,6 @@ class UpgradeRoomViewModelTask @Inject constructor(
// autoJoin = currentInfo.autoJoin ?: false, // autoJoin = currentInfo.autoJoin ?: false,
suggested = currentInfo.suggested suggested = currentInfo.suggested
) )
parentSpace.removeChildren(params.roomId)
} }
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {

View File

@ -85,27 +85,27 @@ class EventHtmlRenderer @Inject constructor(
} else { } else {
builder builder
} }
.usePlugin( .usePlugin(
MarkwonInlineParserPlugin.create( MarkwonInlineParserPlugin.create(
/* Configuring the Markwon inline formatting processor. /* Configuring the Markwon inline formatting processor.
* Default settings are all Markdown features. Turn those off, only using the * Default settings are all Markdown features. Turn those off, only using the
* inline HTML processor and HTML entities processor. * inline HTML processor and HTML entities processor.
*/ */
MarkwonInlineParser.factoryBuilderNoDefaults() MarkwonInlineParser.factoryBuilderNoDefaults()
.addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor .addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor
.addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor .addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor
)
) )
) .usePlugin(object : AbstractMarkwonPlugin() {
.usePlugin(object : AbstractMarkwonPlugin() { override fun configureParser(builder: Parser.Builder) {
override fun configureParser(builder: Parser.Builder) { /* Configuring the Markwon block formatting processor.
/* Configuring the Markwon block formatting processor. * Default settings are all Markdown blocks. Turn those off.
* Default settings are all Markdown blocks. Turn those off. */
*/ builder.enabledBlockTypes(kotlin.collections.emptySet())
builder.enabledBlockTypes(kotlin.collections.emptySet()) }
} })
}) .textSetter(PrecomputedFutureTextSetterCompat.create())
.textSetter(PrecomputedFutureTextSetterCompat.create()) .build()
.build()
val plugins: List<MarkwonPlugin> = markwon.plugins val plugins: List<MarkwonPlugin> = markwon.plugins

View File

@ -23,5 +23,6 @@ sealed class LocationSharingAction : VectorViewModelAction {
data class PinnedLocationSharing(val locationData: LocationData?) : LocationSharingAction() data class PinnedLocationSharing(val locationData: LocationData?) : LocationSharingAction()
data class LocationTargetChange(val locationData: LocationData) : LocationSharingAction() data class LocationTargetChange(val locationData: LocationData) : LocationSharingAction()
object ZoomToUserLocation : LocationSharingAction() object ZoomToUserLocation : LocationSharingAction()
object LiveLocationSharingRequested : LocationSharingAction()
data class StartLiveLocationSharing(val durationMillis: Long) : LocationSharingAction() data class StartLiveLocationSharing(val durationMillis: Long) : LocationSharingAction()
} }

View File

@ -26,9 +26,12 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.services.VectorAndroidService import im.vector.app.core.services.VectorAndroidService
import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.redaction.CheckIfEventIsRedactedUseCase
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -55,6 +58,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
@Inject lateinit var locationTracker: LocationTracker @Inject lateinit var locationTracker: LocationTracker
@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase @Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase
@Inject lateinit var checkIfEventIsRedactedUseCase: CheckIfEventIsRedactedUseCase
private val binder = LocalBinder() private val binder = LocalBinder()
@ -66,6 +70,9 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
private val jobs = mutableListOf<Job>() private val jobs = mutableListOf<Job>()
private var startInProgress = false private var startInProgress = false
private val _roomIdsOfActiveLives = MutableSharedFlow<Set<String>>(replay = 1)
val roomIdsOfActiveLives = _roomIdsOfActiveLives.asSharedFlow()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Timber.i("onCreate") Timber.i("onCreate")
@ -193,24 +200,30 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) { private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) {
Timber.i("adding roomArgs for beaconEventId: $beaconEventId") Timber.i("adding roomArgs for beaconEventId: $beaconEventId")
roomArgsMap[beaconEventId] = roomArgs roomArgsMap[beaconEventId] = roomArgs
launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) }
} }
private fun removeRoomArgs(beaconEventId: String) { private fun removeRoomArgs(beaconEventId: String) {
Timber.i("removing roomArgs for beaconEventId: $beaconEventId") Timber.i("removing roomArgs for beaconEventId: $beaconEventId")
roomArgsMap.remove(beaconEventId) roomArgsMap.remove(beaconEventId)
launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) }
} }
private fun listenForLiveSummaryChanges(roomId: String, beaconEventId: String) { private fun listenForLiveSummaryChanges(roomId: String, beaconEventId: String) {
launchWithActiveSession { session -> launchWithActiveSession { session ->
val job = getLiveLocationShareSummaryUseCase.execute(roomId, beaconEventId) val job = getLiveLocationShareSummaryUseCase.execute(roomId, beaconEventId)
.distinctUntilChangedBy { it.isActive } .distinctUntilChangedBy { it?.isActive }
.filter { it.isActive == false } .filter { it?.isActive == false || (it == null && isLiveRedacted(roomId, beaconEventId)) }
.onEach { stopSharingLocation(beaconEventId) } .onEach { stopSharingLocation(beaconEventId) }
.launchIn(session.coroutineScope) .launchIn(session.coroutineScope)
jobs.add(job) jobs.add(job)
} }
} }
private suspend fun isLiveRedacted(roomId: String, beaconEventId: String): Boolean {
return checkIfEventIsRedactedUseCase.execute(roomId = roomId, eventId = beaconEventId)
}
private fun launchWithActiveSession(block: suspend CoroutineScope.(Session) -> Unit) = private fun launchWithActiveSession(block: suspend CoroutineScope.(Session) -> Unit) =
activeSessionHolder activeSessionHolder
.getSafeActiveSession() .getSafeActiveSession()
@ -220,6 +233,10 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
) )
} }
fun getRoomIdsOfActiveLives(): Set<String> {
return roomArgsMap.map { it.value.roomId }.toSet()
}
override fun onBind(intent: Intent?): IBinder { override fun onBind(intent: Intent?): IBinder {
return binder return binder
} }

View File

@ -104,6 +104,9 @@ class LocationSharingFragment @Inject constructor(
LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError() LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it) is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it)
is LocationSharingViewEvents.StartLiveLocationService -> handleStartLiveLocationService(it) is LocationSharingViewEvents.StartLiveLocationService -> handleStartLiveLocationService(it)
LocationSharingViewEvents.ChooseLiveLocationDuration -> handleChooseLiveLocationDuration()
LocationSharingViewEvents.ShowLabsFlagPromotion -> handleShowLabsFlagPromotion()
LocationSharingViewEvents.LiveLocationSharingNotEnoughPermission -> handleLiveLocationSharingNotEnoughPermission()
} }
} }
} }
@ -168,6 +171,14 @@ class LocationSharingFragment @Inject constructor(
.show() .show()
} }
private fun handleLiveLocationSharingNotEnoughPermission() {
MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.live_location_not_enough_permission_dialog_title)
.setMessage(R.string.live_location_not_enough_permission_dialog_description)
.setPositiveButton(R.string.ok, null)
.show()
}
private fun initLocateButton() { private fun initLocateButton() {
views.mapView.locateButton.setOnClickListener { views.mapView.locateButton.setOnClickListener {
viewModel.handle(LocationSharingAction.ZoomToUserLocation) viewModel.handle(LocationSharingAction.ZoomToUserLocation)
@ -201,7 +212,7 @@ class LocationSharingFragment @Inject constructor(
viewModel.handle(LocationSharingAction.CurrentUserLocationSharing) viewModel.handle(LocationSharingAction.CurrentUserLocationSharing)
} }
views.shareLocationOptionsPicker.optionUserLive.debouncedClicks { views.shareLocationOptionsPicker.optionUserLive.debouncedClicks {
tryStartLiveLocationSharing() viewModel.handle(LocationSharingAction.LiveLocationSharingRequested)
} }
} }
@ -212,13 +223,13 @@ class LocationSharingFragment @Inject constructor(
} }
} }
private fun tryStartLiveLocationSharing() { private fun handleChooseLiveLocationDuration() {
if (vectorPreferences.labsEnableLiveLocation()) { startLiveLocationSharing()
startLiveLocationSharing() }
} else {
LiveLocationLabsFlagPromotionBottomSheet.newInstance() private fun handleShowLabsFlagPromotion() {
.show(requireActivity().supportFragmentManager, "DISPLAY_LIVE_LOCATION_LABS_FLAG_PROMOTION") LiveLocationLabsFlagPromotionBottomSheet.newInstance()
} .show(requireActivity().supportFragmentManager, "DISPLAY_LIVE_LOCATION_LABS_FLAG_PROMOTION")
} }
private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->

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