forked from GitHub-Mirror/riotX-android
Merge branch 'develop' into feature/create_direct_room
This commit is contained in:
@ -99,14 +99,14 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.1.0-beta01"
|
||||
implementation "androidx.recyclerview:recyclerview:1.1.0-alpha06"
|
||||
implementation "androidx.appcompat:appcompat:1.1.0-rc01"
|
||||
implementation "androidx.recyclerview:recyclerview:1.1.0-beta01"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
|
||||
|
||||
// Network
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.14.1'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
|
||||
|
@ -57,6 +57,9 @@ interface Session :
|
||||
*/
|
||||
val sessionParams: SessionParams
|
||||
|
||||
/**
|
||||
* Useful shortcut to get access to the userId
|
||||
*/
|
||||
val myUserId: String
|
||||
get() = sessionParams.credentials.userId
|
||||
|
||||
@ -84,7 +87,7 @@ interface Session :
|
||||
/**
|
||||
* This method start the sync thread.
|
||||
*/
|
||||
fun startSync()
|
||||
fun startSync(fromForeground : Boolean)
|
||||
|
||||
/**
|
||||
* This method stop the sync thread.
|
||||
|
@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.events.model
|
||||
|
||||
|
||||
/**
|
||||
* Constants defining known event relation types from Matrix specifications.
|
||||
* Constants defining known event relation types from Matrix specifications
|
||||
*/
|
||||
object RelationType {
|
||||
|
||||
@ -25,7 +25,7 @@ object RelationType {
|
||||
const val ANNOTATION = "m.annotation"
|
||||
/** Lets you define an event which replaces an existing event.*/
|
||||
const val REPLACE = "m.replace"
|
||||
/** ets you define an event which references an existing event.*/
|
||||
/** Lets you define an event which references an existing event.*/
|
||||
const val REFERENCE = "m.reference"
|
||||
|
||||
}
|
@ -25,4 +25,9 @@ interface MessageContent {
|
||||
val body: String
|
||||
val relatesTo: RelationDefaultContent?
|
||||
val newContent: Content?
|
||||
}
|
||||
|
||||
|
||||
fun MessageContent?.isReply(): Boolean {
|
||||
return this?.relatesTo?.inReplyTo != null
|
||||
}
|
@ -16,7 +16,10 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model.relation
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||
|
||||
interface RelationContent {
|
||||
/** See [RelationType] for known possible values */
|
||||
val type: String?
|
||||
val eventId: String?
|
||||
val inReplyTo: ReplyToContent?
|
||||
|
@ -80,6 +80,22 @@ interface RelationService {
|
||||
newBodyAutoMarkdown: Boolean,
|
||||
compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
||||
|
||||
|
||||
/**
|
||||
* Edit a reply. This is a special case because replies contains fallback text as a prefix.
|
||||
* This method will take the new body (stripped from fallbacks) and re-add them before sending.
|
||||
* @param replyToEdit The event to edit
|
||||
* @param originalSenderId the sender of the message that this reply (being edited) is relating to
|
||||
* @param originalEventId the event id that this reply (being edited) is relating to
|
||||
* @param newBodyText The edited body (stripped from in reply to content)
|
||||
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
|
||||
*/
|
||||
fun editReply(replyToEdit: TimelineEvent,
|
||||
originalSenderId: String?,
|
||||
originalEventId : String,
|
||||
newBodyText: String,
|
||||
compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
||||
|
||||
/**
|
||||
* Get's the edit history of the given event
|
||||
*/
|
||||
|
@ -21,7 +21,9 @@ import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.isReply
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
|
||||
|
||||
/**
|
||||
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
|
||||
@ -88,3 +90,15 @@ data class TimelineEvent(
|
||||
*/
|
||||
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: root.getClearContent().toModel()
|
||||
|
||||
|
||||
fun TimelineEvent.getTextEditableContent(): String? {
|
||||
val originalContent = root.getClearContent().toModel<MessageContent>() ?: return null
|
||||
val isReply = originalContent.isReply()
|
||||
val lastContent = getLastMessageContent()
|
||||
return if (isReply) {
|
||||
return extractUsefulTextFromReply(lastContent?.body ?: "")
|
||||
} else {
|
||||
lastContent?.body ?: ""
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2019 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.matrix.android.api.util
|
||||
|
||||
|
||||
object ContentUtils {
|
||||
fun extractUsefulTextFromReply(repliedBody: String): String {
|
||||
val lines = repliedBody.lines()
|
||||
var wellFormed = repliedBody.startsWith(">")
|
||||
var endOfPreviousFound = false
|
||||
val usefullines = ArrayList<String>()
|
||||
lines.forEach {
|
||||
if (it == "") {
|
||||
endOfPreviousFound = true
|
||||
return@forEach
|
||||
}
|
||||
if (!endOfPreviousFound) {
|
||||
wellFormed = wellFormed && it.startsWith(">")
|
||||
} else {
|
||||
usefullines.add(it)
|
||||
}
|
||||
}
|
||||
return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
|
||||
}
|
||||
|
||||
fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
|
||||
if (repliedBody.startsWith("<mx-reply>")) {
|
||||
val closingTagIndex = repliedBody.lastIndexOf("</mx-reply>")
|
||||
if (closingTagIndex != -1)
|
||||
return repliedBody.substring(closingTagIndex + "</mx-reply>".length).trim()
|
||||
}
|
||||
return repliedBody
|
||||
}
|
||||
}
|
@ -50,16 +50,10 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
|
||||
}
|
||||
|
||||
private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
|
||||
val userId = sessionParams.credentials.userId
|
||||
if (sessionComponents.containsKey(userId)) {
|
||||
return sessionComponents[userId]!!
|
||||
return sessionComponents.getOrPut(sessionParams.credentials.userId) {
|
||||
DaggerSessionComponent
|
||||
.factory()
|
||||
.create(matrixComponent, sessionParams)
|
||||
}
|
||||
return DaggerSessionComponent
|
||||
.factory()
|
||||
.create(matrixComponent, sessionParams)
|
||||
.also {
|
||||
sessionComponents[sessionParams.credentials.userId] = it
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -21,7 +21,6 @@ package im.vector.matrix.android.internal.crypto
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import com.squareup.moshi.Types
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
@ -80,10 +79,9 @@ import im.vector.matrix.android.internal.util.fetchCopied
|
||||
import kotlinx.coroutines.*
|
||||
import org.matrix.olm.OlmManager
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* A `CryptoService` class instance manages the end-to-end crypto for a session.
|
||||
@ -248,7 +246,7 @@ internal class CryptoManager @Inject constructor(
|
||||
return
|
||||
}
|
||||
isStarting.set(true)
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
internalStart(isInitialSync)
|
||||
}
|
||||
}
|
||||
@ -315,7 +313,7 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param syncResponse the syncResponse
|
||||
*/
|
||||
fun onSyncCompleted(syncResponse: SyncResponse) {
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
if (syncResponse.deviceLists != null) {
|
||||
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
|
||||
}
|
||||
@ -340,7 +338,7 @@ internal class CryptoManager @Inject constructor(
|
||||
* @return the device info, or null if not found / unsupported algorithm / crypto released
|
||||
*/
|
||||
override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? {
|
||||
return if (!TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_MEGOLM) && !TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_OLM)) {
|
||||
return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) {
|
||||
// We only deal in olm keys
|
||||
null
|
||||
} else cryptoStore.deviceWithIdentityKey(senderKey)
|
||||
@ -353,8 +351,8 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param deviceId the device id
|
||||
*/
|
||||
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
|
||||
return if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) {
|
||||
cryptoStore.getUserDevice(deviceId!!, userId)
|
||||
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
|
||||
cryptoStore.getUserDevice(deviceId, userId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -439,7 +437,7 @@ internal class CryptoManager @Inject constructor(
|
||||
// (for now at least. Maybe we should alert the user somehow?)
|
||||
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
|
||||
|
||||
if (!TextUtils.isEmpty(existingAlgorithm) && !TextUtils.equals(existingAlgorithm, algorithm)) {
|
||||
if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) {
|
||||
Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
|
||||
return false
|
||||
}
|
||||
@ -535,7 +533,7 @@ internal class CryptoManager @Inject constructor(
|
||||
eventType: String,
|
||||
roomId: String,
|
||||
callback: MatrixCallback<MXEncryptEventContentResult>) {
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
if (!isStarted()) {
|
||||
Timber.v("## encryptEventContent() : wait after e2e init")
|
||||
internalStart(false)
|
||||
@ -601,7 +599,7 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param callback the callback to return data or null
|
||||
*/
|
||||
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
||||
GlobalScope.launch(EmptyCoroutineContext) {
|
||||
GlobalScope.launch {
|
||||
val result = withContext(coroutineDispatchers.crypto) {
|
||||
internalDecryptEvent(event, timeline)
|
||||
}
|
||||
@ -649,7 +647,7 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param event the event
|
||||
*/
|
||||
fun onToDeviceEvent(event: Event) {
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
when (event.getClearType()) {
|
||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||
onRoomKeyEvent(event)
|
||||
@ -671,7 +669,7 @@ internal class CryptoManager @Inject constructor(
|
||||
*/
|
||||
private fun onRoomKeyEvent(event: Event) {
|
||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
||||
if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.algorithm)) {
|
||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
||||
Timber.e("## onRoomKeyEvent() : missing fields")
|
||||
return
|
||||
}
|
||||
@ -689,7 +687,7 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param event the encryption event.
|
||||
*/
|
||||
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
val params = LoadRoomMembersTask.Params(roomId)
|
||||
loadRoomMembersTask
|
||||
.execute(params)
|
||||
@ -738,7 +736,7 @@ internal class CryptoManager @Inject constructor(
|
||||
val membership = roomMember?.membership
|
||||
if (membership == Membership.JOIN) {
|
||||
// make sure we are tracking the deviceList for this user.
|
||||
deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
|
||||
deviceListManager.startTrackingDeviceList(listOf(userId))
|
||||
} else if (membership == Membership.INVITE
|
||||
&& shouldEncryptForInvitedMembers(roomId)
|
||||
&& cryptoConfig.enableEncryptionForInvitedMembers) {
|
||||
@ -747,7 +745,7 @@ internal class CryptoManager @Inject constructor(
|
||||
// know what other servers are in the room at the time they've been invited.
|
||||
// They therefore will not send device updates if a user logs in whilst
|
||||
// their state is invite.
|
||||
deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
|
||||
deviceListManager.startTrackingDeviceList(listOf(userId))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -782,7 +780,11 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param callback the exported keys
|
||||
*/
|
||||
override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
|
||||
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT, callback)
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
runCatching {
|
||||
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
|
||||
}.fold(callback::onSuccess, callback::onFailure)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -792,30 +794,16 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param anIterationCount the encryption iteration count (0 means no encryption)
|
||||
* @param callback the exported keys
|
||||
*/
|
||||
private fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) {
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
Try {
|
||||
val iterationCount = Math.max(0, anIterationCount)
|
||||
private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray {
|
||||
return withContext(coroutineDispatchers.crypto) {
|
||||
val iterationCount = max(0, anIterationCount)
|
||||
|
||||
val exportedSessions = ArrayList<MegolmSessionData>()
|
||||
val exportedSessions = cryptoStore.getInboundGroupSessions().mapNotNull { it.exportKeys() }
|
||||
|
||||
val inboundGroupSessions = cryptoStore.getInboundGroupSessions()
|
||||
val adapter = MoshiProvider.providesMoshi()
|
||||
.adapter(List::class.java)
|
||||
|
||||
for (session in inboundGroupSessions) {
|
||||
val megolmSessionData = session.exportKeys()
|
||||
|
||||
if (null != megolmSessionData) {
|
||||
exportedSessions.add(megolmSessionData)
|
||||
}
|
||||
}
|
||||
|
||||
val adapter = MoshiProvider.providesMoshi()
|
||||
.adapter(List::class.java)
|
||||
|
||||
MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
|
||||
}
|
||||
}
|
||||
|
||||
@ -879,7 +867,7 @@ internal class CryptoManager @Inject constructor(
|
||||
*/
|
||||
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
|
||||
// force the refresh to ensure that the devices list is up-to-date
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
deviceListManager
|
||||
.downloadKeys(userIds, true)
|
||||
.fold(
|
||||
@ -944,7 +932,7 @@ internal class CryptoManager @Inject constructor(
|
||||
val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList()
|
||||
|
||||
if (add) {
|
||||
if (!roomIds.contains(roomId)) {
|
||||
if (roomId !in roomIds) {
|
||||
roomIds.add(roomId)
|
||||
}
|
||||
} else {
|
||||
@ -1033,8 +1021,7 @@ internal class CryptoManager @Inject constructor(
|
||||
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
val userIds = devicesInRoom.userIds
|
||||
for (userId in userIds) {
|
||||
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
|
||||
deviceIds?.forEach { deviceId ->
|
||||
devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId ->
|
||||
devicesInRoom.getObject(userId, deviceId)
|
||||
?.takeIf { it.isUnknown }
|
||||
?.let {
|
||||
@ -1047,7 +1034,7 @@ internal class CryptoManager @Inject constructor(
|
||||
}
|
||||
|
||||
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
deviceListManager
|
||||
.downloadKeys(userIds, forceDownload)
|
||||
.foldToCallback(callback)
|
||||
|
@ -29,7 +29,6 @@ import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevi
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||
@ -38,10 +37,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||
@ -312,7 +310,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||
return
|
||||
}
|
||||
val userId = request.userId ?: return
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
deviceListManager
|
||||
.downloadKeys(listOf(userId), false)
|
||||
.flatMap {
|
||||
@ -321,8 +319,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||
if (deviceInfo == null) {
|
||||
throw RuntimeException()
|
||||
} else {
|
||||
val devicesByUser = HashMap<String, List<MXDeviceInfo>>()
|
||||
devicesByUser[userId] = ArrayList(Arrays.asList(deviceInfo))
|
||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
||||
ensureOlmSessionsForDevicesAction
|
||||
.handle(devicesByUser)
|
||||
.flatMap {
|
||||
@ -336,8 +333,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||
Timber.v("## shareKeysWithDevice() : sharing keys for session" +
|
||||
" ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId")
|
||||
|
||||
val payloadJson = HashMap<String, Any>()
|
||||
payloadJson["type"] = EventType.FORWARDED_ROOM_KEY
|
||||
val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
|
||||
|
||||
olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId)
|
||||
.fold(
|
||||
@ -350,7 +346,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||
}
|
||||
)
|
||||
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, Arrays.asList(deviceInfo))
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
||||
Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId")
|
||||
|
@ -40,7 +40,7 @@ import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
@ -71,7 +71,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
|
||||
|
||||
// Event received from the sync
|
||||
fun onToDeviceEvent(event: Event) {
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START -> {
|
||||
onStartRequestReceived(event)
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.database.helper
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
@ -103,7 +102,6 @@ internal fun ChunkEntity.updateSenderDataFor(eventIds: List<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun ChunkEntity.add(roomId: String,
|
||||
event: Event,
|
||||
direction: PaginationDirection,
|
||||
@ -134,7 +132,7 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
}
|
||||
}
|
||||
|
||||
val localId = TimelineEventEntity.nextId(realm)
|
||||
val localId = TimelineEventEntity.nextId(realm)
|
||||
val eventEntity = TimelineEventEntity(localId).also {
|
||||
it.root = event.toEntity(roomId).apply {
|
||||
this.stateIndex = currentStateIndex
|
||||
|
@ -37,25 +37,22 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
|
||||
stateIndex: Int = Int.MIN_VALUE,
|
||||
filterDuplicates: Boolean = false,
|
||||
isUnlinked: Boolean = false) {
|
||||
internal fun RoomEntity.addStateEvent(stateEvent: Event,
|
||||
stateIndex: Int = Int.MIN_VALUE,
|
||||
filterDuplicates: Boolean = false,
|
||||
isUnlinked: Boolean = false) {
|
||||
assertIsManaged()
|
||||
|
||||
stateEvents.forEach { event ->
|
||||
if (event.eventId == null || (filterDuplicates && fastContains(event.eventId))) {
|
||||
return@forEach
|
||||
}
|
||||
val eventEntity = event.toEntity(roomId).apply {
|
||||
if (stateEvent.eventId == null || (filterDuplicates && fastContains(stateEvent.eventId))) {
|
||||
return
|
||||
} else {
|
||||
val entity = stateEvent.toEntity(roomId).apply {
|
||||
this.stateIndex = stateIndex
|
||||
this.isUnlinked = isUnlinked
|
||||
this.sendState = SendState.SYNCED
|
||||
}
|
||||
untimelinedStateEvents.add(0, eventEntity)
|
||||
untimelinedStateEvents.add(entity)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun RoomEntity.addSendingEvent(event: Event) {
|
||||
assertIsManaged()
|
||||
val senderId = event.senderId ?: return
|
||||
@ -64,7 +61,7 @@ internal fun RoomEntity.addSendingEvent(event: Event) {
|
||||
}
|
||||
val roomMembers = RoomMembers(realm, roomId)
|
||||
val myUser = roomMembers.get(senderId)
|
||||
val localId = TimelineEventEntity.nextId(realm)
|
||||
val localId = TimelineEventEntity.nextId(realm)
|
||||
val timelineEventEntity = TimelineEventEntity(localId).also {
|
||||
it.root = eventEntity
|
||||
it.eventId = event.eventId ?: ""
|
||||
|
@ -20,8 +20,6 @@ import io.realm.RealmObject
|
||||
import io.realm.RealmResults
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.LinkingObjects
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import java.util.*
|
||||
|
||||
|
||||
internal open class TimelineEventEntity(var localId: Long = 0,
|
||||
|
@ -19,21 +19,15 @@ package im.vector.matrix.android.internal.network
|
||||
import arrow.core.Try
|
||||
import arrow.core.failure
|
||||
import arrow.core.recoverWith
|
||||
import arrow.effects.IO
|
||||
import arrow.effects.fix
|
||||
import arrow.effects.instances.io.async.async
|
||||
import arrow.integrations.retrofit.adapter.runAsync
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.MatrixError
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Call
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
internal suspend inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute()
|
||||
|
||||
@ -43,30 +37,22 @@ internal class Request<DATA> {
|
||||
lateinit var apiCall: Call<DATA>
|
||||
|
||||
suspend fun execute(): Try<DATA> {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
Timber.v("Request is canceled")
|
||||
apiCall.cancel()
|
||||
return Try {
|
||||
val response = apiCall.awaitResponse()
|
||||
if (response.isSuccessful) {
|
||||
response.body()
|
||||
?: throw IllegalStateException("The request returned a null body")
|
||||
} else {
|
||||
throw manageFailure(response.errorBody(), response.code())
|
||||
}
|
||||
val result = Try {
|
||||
val response = apiCall.runAsync(IO.async()).fix().unsafeRunSync()
|
||||
if (response.isSuccessful) {
|
||||
response.body()
|
||||
?: throw IllegalStateException("The request returned a null body")
|
||||
} else {
|
||||
throw manageFailure(response.errorBody(), response.code())
|
||||
}
|
||||
}.recoverWith {
|
||||
when (it) {
|
||||
is IOException -> Failure.NetworkConnection(it)
|
||||
is Failure.ServerError,
|
||||
is Failure.OtherServerError -> it
|
||||
else -> Failure.Unknown(it)
|
||||
}.failure()
|
||||
}
|
||||
continuation.resume(result)
|
||||
}.recoverWith {
|
||||
when (it) {
|
||||
is IOException -> Failure.NetworkConnection(it)
|
||||
is Failure.ServerError,
|
||||
is Failure.OtherServerError -> it
|
||||
else -> Failure.Unknown(it)
|
||||
}.failure()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable {
|
||||
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 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.matrix.android.internal.network
|
||||
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import retrofit2.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
suspend fun <T> Call<T>.awaitResponse(): Response<T> {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
cancel()
|
||||
}
|
||||
enqueue(object : Callback<T> {
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
continuation.resume(response)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||
continuation.resumeWithException(t)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -94,19 +94,21 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
|
||||
}
|
||||
|
||||
override fun requireBackgroundSync() {
|
||||
SyncWorker.requireBackgroundSync(context, sessionParams.credentials.userId)
|
||||
SyncWorker.requireBackgroundSync(context, myUserId)
|
||||
}
|
||||
|
||||
override fun startAutomaticBackgroundSync(repeatDelay: Long) {
|
||||
SyncWorker.automaticallyBackgroundSync(context, sessionParams.credentials.userId, 0, repeatDelay)
|
||||
SyncWorker.automaticallyBackgroundSync(context, myUserId, 0, repeatDelay)
|
||||
}
|
||||
|
||||
override fun stopAnyBackgroundSync() {
|
||||
SyncWorker.stopAnyBackgroundSync(context)
|
||||
}
|
||||
|
||||
override fun startSync() {
|
||||
override fun startSync(fromForeground : Boolean) {
|
||||
Timber.i("Starting sync thread")
|
||||
assert(isOpen)
|
||||
syncThread.setInitialForeground(fromForeground)
|
||||
if (!syncThread.isAlive) {
|
||||
syncThread.start()
|
||||
} else {
|
||||
|
@ -38,7 +38,6 @@ import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
|
||||
import im.vector.matrix.android.internal.session.user.UserEntityUpdater
|
||||
import im.vector.matrix.android.internal.util.md5
|
||||
import io.realm.RealmConfiguration
|
||||
import okhttp3.OkHttpClient
|
||||
@ -129,10 +128,6 @@ internal abstract class SessionModule {
|
||||
@IntoSet
|
||||
abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindUserEntityUpdater(groupSummaryUpdater: UserEntityUpdater): LiveEntityObserver
|
||||
|
||||
@Binds
|
||||
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService
|
||||
|
||||
|
@ -20,14 +20,13 @@ import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomAvatarContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.prev
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -42,32 +41,25 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona
|
||||
fun resolve(roomId: String): String? {
|
||||
var res: String? = null
|
||||
monarchy.doWithRealm { realm ->
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev()?.asDomain()
|
||||
res = roomName?.content.toModel<RoomAvatarContent>()?.avatarUrl
|
||||
if (!res.isNullOrEmpty()) {
|
||||
return@doWithRealm
|
||||
}
|
||||
val roomMembers = RoomMembers(realm, roomId)
|
||||
val members = roomMembers.getLoaded()
|
||||
if (roomEntity?.membership == Membership.INVITE) {
|
||||
if (members.size == 1) {
|
||||
res = members.entries.first().value.avatarUrl
|
||||
} else if (members.size > 1) {
|
||||
val firstOtherMember = members.filterKeys { it != credentials.userId }.values.firstOrNull()
|
||||
res = firstOtherMember?.avatarUrl
|
||||
}
|
||||
} else {
|
||||
// detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat)
|
||||
if (members.size == 1) {
|
||||
res = members.entries.first().value.avatarUrl
|
||||
} else if (members.size == 2) {
|
||||
val firstOtherMember = members.filterKeys { it != credentials.userId }.values.firstOrNull()
|
||||
res = firstOtherMember?.avatarUrl
|
||||
}
|
||||
val members = roomMembers.queryRoomMembersEvent().findAll()
|
||||
// detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat)
|
||||
if (members.size == 1) {
|
||||
res = members.firstOrNull()?.toRoomMember()?.avatarUrl
|
||||
} else if (members.size == 2) {
|
||||
val firstOtherMember = members.where().notEqualTo(EventEntityFields.STATE_KEY, credentials.userId).findFirst()
|
||||
res = firstOtherMember?.toRoomMember()?.avatarUrl
|
||||
}
|
||||
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
private fun EventEntity?.toRoomMember(): RoomMember? {
|
||||
return this?.asDomain()?.content?.toModel<RoomMember>()
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||
@ -86,12 +87,20 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
|
||||
|
||||
val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includedTypes = PREVIEWABLE_TYPES)
|
||||
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()
|
||||
val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId }
|
||||
|
||||
val otherRoomMembers = RoomMembers(realm, roomId)
|
||||
.queryRoomMembersEvent()
|
||||
.notEqualTo(EventEntityFields.STATE_KEY, credentials.userId)
|
||||
.findAll()
|
||||
.asSequence()
|
||||
.map { it.stateKey }
|
||||
|
||||
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
|
||||
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
|
||||
roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
|
||||
roomSummaryEntity.latestEvent = latestEvent
|
||||
roomSummaryEntity.otherMemberIds.clear()
|
||||
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers.keys)
|
||||
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
|
||||
|
||||
}
|
||||
}
|
@ -17,9 +17,10 @@
|
||||
package im.vector.matrix.android.internal.session.room.membership
|
||||
|
||||
import arrow.core.Try
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.internal.database.helper.addStateEvents
|
||||
import im.vector.matrix.android.internal.database.helper.addStateEvent
|
||||
import im.vector.matrix.android.internal.database.helper.updateSenderData
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
@ -27,10 +28,13 @@ import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
|
||||
import im.vector.matrix.android.internal.session.user.UserEntityFactory
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import okhttp3.ResponseBody
|
||||
import okio.Okio
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolean> {
|
||||
@ -60,23 +64,26 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertInDb(response: RoomMembersResponse, roomId: String): Try<RoomMembersResponse> {
|
||||
private fun insertInDb(response: RoomMembersResponse, roomId: String): Try<Unit> {
|
||||
return monarchy
|
||||
.tryTransactionSync { realm ->
|
||||
// We ignore all the already known members
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
val roomMembers = RoomMembers(realm, roomId).getLoaded()
|
||||
val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) }
|
||||
roomEntity.addStateEvents(eventsToInsert)
|
||||
|
||||
for (roomMemberEvent in response.roomMemberEvents) {
|
||||
roomEntity.addStateEvent(roomMemberEvent)
|
||||
UserEntityFactory.createOrNull(roomMemberEvent)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
roomEntity.chunks.flatMap { it.timelineEvents }.forEach {
|
||||
it.updateSenderData()
|
||||
}
|
||||
roomEntity.areAllMembersLoaded = true
|
||||
roomSummaryUpdater.update(realm, roomId)
|
||||
}
|
||||
.map { response }
|
||||
}
|
||||
|
||||
private fun areAllMembersAlreadyLoaded(roomId: String): Boolean {
|
||||
@ -85,4 +92,4 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -25,13 +25,16 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.query.prev
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import io.realm.RealmResults
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@ -39,7 +42,6 @@ import javax.inject.Inject
|
||||
*/
|
||||
internal class RoomDisplayNameResolver @Inject constructor(private val context: Context,
|
||||
private val monarchy: Monarchy,
|
||||
private val roomMemberDisplayNameResolver: RoomMemberDisplayNameResolver,
|
||||
private val credentials: Credentials
|
||||
) {
|
||||
|
||||
@ -78,48 +80,61 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
|
||||
}
|
||||
|
||||
val roomMembers = RoomMembers(realm, roomId)
|
||||
val loadedMembers = roomMembers.getLoaded()
|
||||
val otherRoomMembers = loadedMembers.filterKeys { it != credentials.userId }
|
||||
val loadedMembers = roomMembers.queryRoomMembersEvent().findAll()
|
||||
val otherMembersSubset = loadedMembers.where()
|
||||
.notEqualTo(EventEntityFields.STATE_KEY, credentials.userId)
|
||||
.limit(3)
|
||||
.findAll()
|
||||
|
||||
if (roomEntity?.membership == Membership.INVITE) {
|
||||
val inviteMeEvent = roomMembers.queryRoomMemberEvent(credentials.userId).findFirst()
|
||||
val inviterId = inviteMeEvent?.sender
|
||||
name = if (inviterId != null && otherRoomMembers.containsKey(inviterId)) {
|
||||
roomMemberDisplayNameResolver.resolve(inviterId, otherRoomMembers)
|
||||
name = if (inviterId != null) {
|
||||
val inviterMemberEvent = loadedMembers.where()
|
||||
.equalTo(EventEntityFields.STATE_KEY, inviterId)
|
||||
.findFirst()
|
||||
inviterMemberEvent?.toRoomMember()?.displayName
|
||||
} else {
|
||||
context.getString(R.string.room_displayname_room_invite)
|
||||
}
|
||||
} else {
|
||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
val memberIds = if (roomSummary?.heroes?.isNotEmpty() == true) {
|
||||
val memberIds: List<String> = if (roomSummary?.heroes?.isNotEmpty() == true) {
|
||||
roomSummary.heroes
|
||||
} else {
|
||||
otherRoomMembers.keys.toList()
|
||||
otherMembersSubset.mapNotNull { it.stateKey }
|
||||
}
|
||||
|
||||
val nbOfOtherMembers = memberIds.size
|
||||
|
||||
when (nbOfOtherMembers) {
|
||||
0 -> name = context.getString(R.string.room_displayname_empty_room)
|
||||
1 -> name = roomMemberDisplayNameResolver.resolve(memberIds[0], otherRoomMembers)
|
||||
2 -> {
|
||||
val member1 = memberIds[0]
|
||||
val member2 = memberIds[1]
|
||||
name = context.getString(R.string.room_displayname_two_members,
|
||||
roomMemberDisplayNameResolver.resolve(member1, otherRoomMembers),
|
||||
roomMemberDisplayNameResolver.resolve(member2, otherRoomMembers)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val member = memberIds[0]
|
||||
name = context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
|
||||
roomMembers.getNumberOfJoinedMembers() - 1,
|
||||
roomMemberDisplayNameResolver.resolve(member, otherRoomMembers),
|
||||
roomMembers.getNumberOfJoinedMembers() - 1)
|
||||
}
|
||||
name = when (memberIds.size) {
|
||||
0 -> context.getString(R.string.room_displayname_empty_room)
|
||||
1 -> resolveRoomMember(otherMembersSubset[0], roomMembers)
|
||||
2 -> context.getString(R.string.room_displayname_two_members,
|
||||
resolveRoomMember(otherMembersSubset[0], roomMembers),
|
||||
resolveRoomMember(otherMembersSubset[1], roomMembers)
|
||||
)
|
||||
else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
|
||||
roomMembers.getNumberOfJoinedMembers() - 1,
|
||||
resolveRoomMember(otherMembersSubset[0], roomMembers),
|
||||
roomMembers.getNumberOfJoinedMembers() - 1)
|
||||
}
|
||||
}
|
||||
return@doWithRealm
|
||||
}
|
||||
return name ?: roomId
|
||||
}
|
||||
|
||||
private fun resolveRoomMember(eventEntity: EventEntity?,
|
||||
roomMembers: RoomMembers): String? {
|
||||
if (eventEntity == null) return null
|
||||
val roomMember = eventEntity.toRoomMember() ?: return null
|
||||
val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName)
|
||||
return if (isUnique) {
|
||||
roomMember.displayName
|
||||
} else {
|
||||
"${roomMember.displayName} (${eventEntity.stateKey})"
|
||||
}
|
||||
}
|
||||
|
||||
private fun EventEntity?.toRoomMember(): RoomMember? {
|
||||
return this?.asDomain()?.content?.toModel<RoomMember>()
|
||||
}
|
||||
}
|
||||
|
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 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.matrix.android.internal.session.room.membership
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomMemberDisplayNameResolver @Inject constructor() {
|
||||
|
||||
fun resolve(userId: String, members: Map<String, RoomMember>): String? {
|
||||
val currentMember = members[userId]
|
||||
var displayName = currentMember?.displayName
|
||||
// Get the user display name from the member list of the room
|
||||
// Do not consider null display name
|
||||
|
||||
if (currentMember != null && !currentMember.displayName.isNullOrEmpty()) {
|
||||
val hasNameCollision = members
|
||||
.filterValues { it != currentMember && it.displayName == currentMember.displayName }
|
||||
.isNotEmpty()
|
||||
if (hasNameCollision) {
|
||||
displayName = "${currentMember.displayName} ( $userId )"
|
||||
}
|
||||
}
|
||||
|
||||
// TODO handle invited users
|
||||
/*else if (null != member && TextUtils.equals(member!!.membership, RoomMember.MEMBERSHIP_INVITE)) {
|
||||
val user = (mDataHandler as MXDataHandler).getUser(userId)
|
||||
if (null != user) {
|
||||
displayName = user!!.displayname
|
||||
}
|
||||
}
|
||||
*/
|
||||
if (displayName == null) {
|
||||
// By default, use the user ID
|
||||
displayName = userId
|
||||
}
|
||||
return displayName
|
||||
}
|
||||
|
||||
}
|
@ -33,6 +33,7 @@ import io.realm.Sort
|
||||
* This class is an helper around STATE_ROOM_MEMBER events.
|
||||
* It allows to get the live membership of a user.
|
||||
*/
|
||||
|
||||
internal class RoomMembers(private val realm: Realm,
|
||||
private val roomId: String
|
||||
) {
|
||||
@ -72,27 +73,27 @@ internal class RoomMembers(private val realm: Realm,
|
||||
.isNotNull(EventEntityFields.CONTENT)
|
||||
}
|
||||
|
||||
fun queryJoinedRoomMembersEvent(): RealmQuery<EventEntity> {
|
||||
return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"join\"")
|
||||
}
|
||||
|
||||
fun queryInvitedRoomMembersEvent(): RealmQuery<EventEntity> {
|
||||
return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"invite\"")
|
||||
}
|
||||
|
||||
fun queryRoomMemberEvent(userId: String): RealmQuery<EventEntity> {
|
||||
return queryRoomMembersEvent()
|
||||
.equalTo(EventEntityFields.STATE_KEY, userId)
|
||||
}
|
||||
|
||||
fun getLoaded(): Map<String, RoomMember> {
|
||||
return queryRoomMembersEvent()
|
||||
.findAll()
|
||||
.map { it.asDomain() }
|
||||
.associateBy { it.stateKey!! }
|
||||
.mapValues { it.value.content.toModel<RoomMember>()!! }
|
||||
}
|
||||
|
||||
fun getNumberOfJoinedMembers(): Int {
|
||||
return roomSummary?.joinedMembersCount
|
||||
?: getLoaded().filterValues { it.membership == Membership.JOIN }.size
|
||||
?: queryJoinedRoomMembersEvent().findAll().size
|
||||
}
|
||||
|
||||
fun getNumberOfInvitedMembers(): Int {
|
||||
return roomSummary?.invitedMembersCount
|
||||
?: getLoaded().filterValues { it.membership == Membership.INVITE }.size
|
||||
?: queryInvitedRoomMembersEvent().findAll().size
|
||||
}
|
||||
|
||||
fun getNumberOfMembers(): Int {
|
||||
@ -133,4 +134,4 @@ internal class RoomMembers(private val realm: Realm,
|
||||
.toList()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
@ -132,6 +133,24 @@ internal class DefaultRelationService @Inject constructor(private val context: C
|
||||
|
||||
}
|
||||
|
||||
override fun editReply(replyToEdit: TimelineEvent,
|
||||
originalSenderId: String?,
|
||||
originalEventId: String,
|
||||
newBodyText: String,
|
||||
compatibilityBodyText: String): Cancelable {
|
||||
val event = eventFactory
|
||||
.createReplaceTextOfReply(roomId,
|
||||
replyToEdit,
|
||||
originalSenderId, originalEventId,
|
||||
newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
|
||||
.also {
|
||||
saveLocalEcho(it)
|
||||
}
|
||||
val workRequest = createSendEventWork(event)
|
||||
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
|
||||
return CancelableWork(context, workRequest.id)
|
||||
}
|
||||
|
||||
override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
|
||||
val params = FetchEditHistoryTask.Params(roomId, eventId)
|
||||
fetchEditHistoryTask.configureWith(params)
|
||||
|
@ -104,6 +104,45 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||
))
|
||||
}
|
||||
|
||||
fun createReplaceTextOfReply(roomId: String, eventReplaced: TimelineEvent,
|
||||
originalSenderId: String?,
|
||||
originalEventId: String,
|
||||
newBodyText: String,
|
||||
newBodyAutoMarkdown: Boolean,
|
||||
msgType: String,
|
||||
compatibilityText: String): Event {
|
||||
val permalink = PermalinkFactory.createPermalink(roomId, originalEventId)
|
||||
val userLink = originalSenderId?.let { PermalinkFactory.createPermalink(it) } ?: ""
|
||||
|
||||
val body = bodyForReply(eventReplaced.getLastMessageContent(), eventReplaced.root.getClearContent().toModel())
|
||||
val replyFormatted = REPLY_PATTERN.format(
|
||||
permalink,
|
||||
stringProvider.getString(R.string.message_reply_to_prefix),
|
||||
userLink,
|
||||
originalSenderId,
|
||||
body.takeFormatted(),
|
||||
createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
|
||||
)
|
||||
//
|
||||
// > <@alice:example.org> This is the original body
|
||||
//
|
||||
val replyFallback = buildReplyFallback(body, originalSenderId, newBodyText)
|
||||
|
||||
return createEvent(roomId,
|
||||
MessageTextContent(
|
||||
type = msgType,
|
||||
body = compatibilityText,
|
||||
relatesTo = RelationDefaultContent(RelationType.REPLACE, eventReplaced.root.eventId),
|
||||
newContent = MessageTextContent(
|
||||
type = msgType,
|
||||
format = MessageType.FORMAT_MATRIX_HTML,
|
||||
body = replyFallback,
|
||||
formattedBody = replyFormatted
|
||||
)
|
||||
.toContent()
|
||||
))
|
||||
}
|
||||
|
||||
fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||
return when (attachment.type) {
|
||||
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment)
|
||||
@ -239,16 +278,8 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||
val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null
|
||||
val userId = eventReplied.root.senderId ?: return null
|
||||
val userLink = PermalinkFactory.createPermalink(userId) ?: return null
|
||||
// <mx-reply>
|
||||
// <blockquote>
|
||||
// <a href="https://matrix.to/#/!somewhere:domain.com/$event:domain.com">In reply to</a>
|
||||
// <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a>
|
||||
// <br />
|
||||
// <!-- This is where the related event's HTML would be. -->
|
||||
// </blockquote>
|
||||
// </mx-reply>
|
||||
// This is where the reply goes.
|
||||
val body = bodyForReply(eventReplied.getLastMessageContent())
|
||||
|
||||
val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.root.getClearContent().toModel())
|
||||
val replyFormatted = REPLY_PATTERN.format(
|
||||
permalink,
|
||||
stringProvider.getString(R.string.message_reply_to_prefix),
|
||||
@ -260,8 +291,22 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||
//
|
||||
// > <@alice:example.org> This is the original body
|
||||
//
|
||||
val replyFallback = buildReplyFallback(body, userId, replyText)
|
||||
|
||||
val eventId = eventReplied.root.eventId ?: return null
|
||||
val content = MessageTextContent(
|
||||
type = MessageType.MSGTYPE_TEXT,
|
||||
format = MessageType.FORMAT_MATRIX_HTML,
|
||||
body = replyFallback,
|
||||
formattedBody = replyFormatted,
|
||||
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
|
||||
)
|
||||
return createEvent(roomId, content)
|
||||
}
|
||||
|
||||
private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {
|
||||
val lines = body.text.split("\n")
|
||||
val replyFallback = StringBuffer("><$userId>")
|
||||
val replyFallback = StringBuffer("><$originalSenderId>")
|
||||
lines.forEachIndexed { index, s ->
|
||||
if (index == 0) {
|
||||
replyFallback.append(" $s")
|
||||
@ -269,23 +314,16 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||
replyFallback.append("\n>$s")
|
||||
}
|
||||
}
|
||||
replyFallback.append("\n\n").append(replyText)
|
||||
|
||||
val eventId = eventReplied.root.eventId ?: return null
|
||||
val content = MessageTextContent(
|
||||
type = MessageType.MSGTYPE_TEXT,
|
||||
format = MessageType.FORMAT_MATRIX_HTML,
|
||||
body = replyFallback.toString(),
|
||||
formattedBody = replyFormatted,
|
||||
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
|
||||
)
|
||||
return createEvent(roomId, content)
|
||||
replyFallback.append("\n\n").append(newBodyText)
|
||||
return replyFallback.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a TextContent used for the fallback event representation in a reply message.
|
||||
* We also pass the original content, because in case of an edit of a reply the last content is not
|
||||
* himself a reply, but it will contain the fallbacks, so we have to trim them.
|
||||
*/
|
||||
private fun bodyForReply(content: MessageContent?): TextContent {
|
||||
private fun bodyForReply(content: MessageContent?, originalContent: MessageContent?): TextContent {
|
||||
when (content?.type) {
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
@ -296,7 +334,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||
formattedText = content.formattedBody
|
||||
}
|
||||
}
|
||||
val isReply = content.relatesTo?.inReplyTo?.eventId != null
|
||||
val isReply = content.isReply() || originalContent.isReply()
|
||||
return if (isReply)
|
||||
TextContent(content.body, formattedText).removeInReplyFallbacks()
|
||||
else
|
||||
@ -353,7 +391,16 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||
companion object {
|
||||
const val LOCAL_ID_PREFIX = "local."
|
||||
|
||||
// No whitespace
|
||||
|
||||
// <mx-reply>
|
||||
// <blockquote>
|
||||
// <a href="https://matrix.to/#/!somewhere:domain.com/$event:domain.com">In reply to</a>
|
||||
// <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a>
|
||||
// <br />
|
||||
// <!-- This is where the related event's HTML would be. -->
|
||||
// </blockquote>
|
||||
// </mx-reply>
|
||||
// No whitespace because currently breaks temporary formatted text to Span
|
||||
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">%s</a><a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
|
||||
|
||||
fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(LOCAL_ID_PREFIX)
|
||||
|
@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.session.room.send
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromHtmlReply
|
||||
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
|
||||
|
||||
/**
|
||||
* Contains a text and eventually a formatted text
|
||||
@ -47,28 +49,4 @@ fun TextContent.removeInReplyFallbacks(): TextContent {
|
||||
)
|
||||
}
|
||||
|
||||
private fun extractUsefulTextFromReply(repliedBody: String): String {
|
||||
val lines = repliedBody.lines()
|
||||
var wellFormed = repliedBody.startsWith(">")
|
||||
var endOfPreviousFound = false
|
||||
val usefullines = ArrayList<String>()
|
||||
lines.forEach {
|
||||
if (it == "") {
|
||||
endOfPreviousFound = true
|
||||
return@forEach
|
||||
}
|
||||
if (!endOfPreviousFound) {
|
||||
wellFormed = wellFormed && it.startsWith(">")
|
||||
} else {
|
||||
usefullines.add(it)
|
||||
}
|
||||
}
|
||||
return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
|
||||
}
|
||||
|
||||
private fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
|
||||
if (repliedBody.startsWith("<mx-reply>")) {
|
||||
return repliedBody.substring(repliedBody.lastIndexOf("</mx-reply>") + "</mx-reply>".length).trim()
|
||||
}
|
||||
return repliedBody
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
|
||||
private const val INITIAL_LOAD_SIZE = 10
|
||||
private const val INITIAL_LOAD_SIZE = 30
|
||||
private const val MIN_FETCHING_COUNT = 30
|
||||
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
|
||||
|
||||
|
@ -18,18 +18,14 @@ package im.vector.matrix.android.internal.session.room.timeline
|
||||
|
||||
import arrow.core.Try
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.internal.database.helper.addAll
|
||||
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
||||
import im.vector.matrix.android.internal.database.helper.addStateEvents
|
||||
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
||||
import im.vector.matrix.android.internal.database.helper.isUnlinked
|
||||
import im.vector.matrix.android.internal.database.helper.merge
|
||||
import im.vector.matrix.android.internal.database.helper.*
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.query.create
|
||||
import im.vector.matrix.android.internal.database.query.find
|
||||
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.user.UserEntityFactory
|
||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||
import io.realm.kotlin.createObject
|
||||
import timber.log.Timber
|
||||
@ -117,7 +113,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
||||
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
val nextToken: String?
|
||||
val prevToken: String?
|
||||
@ -146,15 +142,21 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||
} else {
|
||||
nextChunk?.apply { this.prevToken = prevToken }
|
||||
}
|
||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||
|
||||
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
||||
Timber.v("Reach end of $roomId")
|
||||
currentChunk.isLastBackward = true
|
||||
} else {
|
||||
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
||||
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
||||
|
||||
val eventIds = ArrayList<String>(receivedChunk.events.size)
|
||||
for (event in receivedChunk.events) {
|
||||
event.eventId?.also { eventIds.add(it) }
|
||||
currentChunk.add(roomId, event, direction, isUnlinked = currentChunk.isUnlinked())
|
||||
UserEntityFactory.createOrNull(event)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
// Then we merge chunks if needed
|
||||
if (currentChunk != prevChunk && prevChunk != null) {
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
||||
@ -170,7 +172,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||
}
|
||||
}
|
||||
roomEntity.addOrUpdate(currentChunk)
|
||||
roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = currentChunk.isUnlinked())
|
||||
for (stateEvent in receivedChunk.stateEvents) {
|
||||
roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked())
|
||||
UserEntityFactory.createOrNull(stateEvent)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
currentChunk.updateSenderDataFor(eventIds)
|
||||
}
|
||||
}
|
||||
.map {
|
||||
|
@ -24,12 +24,11 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
|
||||
import im.vector.matrix.android.internal.crypto.CryptoManager
|
||||
import im.vector.matrix.android.internal.database.helper.addAll
|
||||
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
||||
import im.vector.matrix.android.internal.database.helper.addStateEvents
|
||||
import im.vector.matrix.android.internal.database.helper.lastStateIndex
|
||||
import im.vector.matrix.android.internal.database.helper.*
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.UserEntity
|
||||
import im.vector.matrix.android.internal.database.query.find
|
||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
@ -40,6 +39,7 @@ import im.vector.matrix.android.internal.session.notification.ProcessEventForPus
|
||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.session.sync.model.*
|
||||
import im.vector.matrix.android.internal.session.user.UserEntityFactory
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import io.realm.Realm
|
||||
@ -125,51 +125,31 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
}
|
||||
roomEntity.membership = Membership.JOIN
|
||||
|
||||
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||
val isInitialSync = lastChunk == null
|
||||
val lastStateIndex = lastChunk?.lastStateIndex(PaginationDirection.FORWARDS) ?: 0
|
||||
val numberOfStateEvents = roomSync.state?.events?.size ?: 0
|
||||
val stateIndexOffset = lastStateIndex + numberOfStateEvents
|
||||
|
||||
// State event
|
||||
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
|
||||
val untimelinedStateIndex = if (isInitialSync) Int.MIN_VALUE else stateIndexOffset
|
||||
roomEntity.addStateEvents(roomSync.state.events, filterDuplicates = true, stateIndex = untimelinedStateIndex)
|
||||
|
||||
// Give info to crypto module
|
||||
roomSync.state.events.forEach {
|
||||
cryptoManager.onStateEvent(roomId, it)
|
||||
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
|
||||
?: Int.MIN_VALUE
|
||||
val untimelinedStateIndex = minStateIndex + 1
|
||||
roomSync.state.events.forEach { event ->
|
||||
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
|
||||
// Give info to crypto module
|
||||
cryptoManager.onStateEvent(roomId, event)
|
||||
UserEntityFactory.createOrNull(event)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
|
||||
val timelineStateOffset = if (isInitialSync || roomSync.timeline.limited.not()) 0 else stateIndexOffset
|
||||
val chunkEntity = handleTimelineEvents(
|
||||
realm,
|
||||
roomId,
|
||||
roomEntity,
|
||||
roomSync.timeline.events,
|
||||
roomSync.timeline.prevToken,
|
||||
roomSync.timeline.limited,
|
||||
timelineStateOffset
|
||||
0
|
||||
)
|
||||
roomEntity.addOrUpdate(chunkEntity)
|
||||
|
||||
// Give info to crypto module
|
||||
roomSync.timeline.events.forEach {
|
||||
cryptoManager.onLiveEvent(roomId, it)
|
||||
}
|
||||
|
||||
// Try to remove local echo
|
||||
val transactionIds = roomSync.timeline.events.mapNotNull { it.unsignedData?.transactionId }
|
||||
transactionIds.forEach {
|
||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
||||
if (sendingEventEntity != null) {
|
||||
Timber.v("Remove local echo for tx:$it")
|
||||
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
|
||||
} else {
|
||||
Timber.v("Can't find corresponding local echo for tx:$it")
|
||||
}
|
||||
}
|
||||
}
|
||||
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)
|
||||
|
||||
@ -192,7 +172,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
?: realm.createObject(roomId)
|
||||
roomEntity.membership = Membership.INVITE
|
||||
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
|
||||
val chunkEntity = handleTimelineEvents(realm, roomId, roomSync.inviteState.events)
|
||||
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
|
||||
roomEntity.addOrUpdate(chunkEntity)
|
||||
}
|
||||
roomSummaryUpdater.update(realm, roomId, Membership.INVITE)
|
||||
@ -212,13 +192,13 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
}
|
||||
|
||||
private fun handleTimelineEvents(realm: Realm,
|
||||
roomId: String,
|
||||
roomEntity: RoomEntity,
|
||||
eventList: List<Event>,
|
||||
prevToken: String? = null,
|
||||
isLimited: Boolean = true,
|
||||
stateIndexOffset: Int = 0): ChunkEntity {
|
||||
|
||||
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
|
||||
val chunkEntity = if (!isLimited && lastChunk != null) {
|
||||
lastChunk
|
||||
} else {
|
||||
@ -226,13 +206,32 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
}
|
||||
lastChunk?.isLastForward = false
|
||||
chunkEntity.isLastForward = true
|
||||
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)
|
||||
|
||||
//update eventAnnotationSummary here?
|
||||
|
||||
val eventIds = ArrayList<String>(eventList.size)
|
||||
for (event in eventList) {
|
||||
event.eventId?.also { eventIds.add(it) }
|
||||
chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)
|
||||
// Give info to crypto module
|
||||
cryptoManager.onLiveEvent(roomEntity.roomId, event)
|
||||
// Try to remove local echo
|
||||
event.unsignedData?.transactionId?.also {
|
||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
||||
if (sendingEventEntity != null) {
|
||||
Timber.v("Remove local echo for tx:$it")
|
||||
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
|
||||
} else {
|
||||
Timber.v("Can't find corresponding local echo for tx:$it")
|
||||
}
|
||||
}
|
||||
UserEntityFactory.createOrNull(event)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
chunkEntity.updateSenderDataFor(eventIds)
|
||||
return chunkEntity
|
||||
}
|
||||
|
||||
|
||||
private fun handleEphemeral(realm: Realm,
|
||||
roomId: String,
|
||||
ephemeral: RoomSyncEphemeral) {
|
||||
|
@ -20,8 +20,6 @@ import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.user.DefaultUpdateUserTask
|
||||
import im.vector.matrix.android.internal.session.user.UpdateUserTask
|
||||
import retrofit2.Retrofit
|
||||
|
||||
@Module
|
||||
|
@ -54,6 +54,10 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
updateStateTo(SyncState.IDLE)
|
||||
}
|
||||
|
||||
fun setInitialForeground(initialForground : Boolean) {
|
||||
updateStateTo(if (initialForground) SyncState.IDLE else SyncState.PAUSED)
|
||||
}
|
||||
|
||||
fun restart() = synchronized(lock) {
|
||||
if (state is SyncState.PAUSED) {
|
||||
Timber.v("Resume sync...")
|
||||
@ -84,7 +88,6 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
Timber.v("Start syncing...")
|
||||
networkConnectivityChecker.register(this)
|
||||
backgroundDetectionObserver.register(this)
|
||||
updateStateTo(SyncState.RUNNING(catchingUp = true))
|
||||
|
||||
while (state != SyncState.KILLING) {
|
||||
if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) {
|
||||
@ -93,7 +96,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
lock.wait()
|
||||
}
|
||||
} else {
|
||||
Timber.v("Execute sync request with timeout $DEFAULT_LONG_POOL_TIMEOUT")
|
||||
updateStateTo(SyncState.RUNNING(catchingUp = true))
|
||||
Timber.v("[$this] Execute sync request with timeout $DEFAULT_LONG_POOL_TIMEOUT")
|
||||
val latch = CountDownLatch(1)
|
||||
val params = SyncTask.Params(DEFAULT_LONG_POOL_TIMEOUT)
|
||||
cancelableTask = syncTask.configureWith(params)
|
||||
@ -148,6 +152,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
}
|
||||
|
||||
private fun updateStateTo(newState: SyncState) {
|
||||
Timber.v("Update state to $newState")
|
||||
state = newState
|
||||
liveState.postValue(newState)
|
||||
}
|
||||
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 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.matrix.android.internal.session.user
|
||||
|
||||
import arrow.core.Try
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.UserEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface UpdateUserTask : Task<UpdateUserTask.Params, Unit> {
|
||||
|
||||
data class Params(val eventIds: List<String>)
|
||||
|
||||
}
|
||||
|
||||
internal class DefaultUpdateUserTask @Inject constructor(private val monarchy: Monarchy) : UpdateUserTask {
|
||||
|
||||
override suspend fun execute(params: UpdateUserTask.Params): Try<Unit> {
|
||||
return monarchy.tryTransactionSync { realm ->
|
||||
params.eventIds.forEach { eventId ->
|
||||
val event = EventEntity.where(realm, eventId).findFirst()?.asDomain()
|
||||
?: return@forEach
|
||||
val roomId = event.roomId ?: return@forEach
|
||||
val userId = event.stateKey ?: return@forEach
|
||||
val roomMember = RoomMembers(realm, roomId).get(userId) ?: return@forEach
|
||||
if (roomMember.membership != Membership.JOIN) return@forEach
|
||||
|
||||
val userEntity = UserEntity.where(realm, userId).findFirst()
|
||||
?: realm.createObject(UserEntity::class.java, userId)
|
||||
userEntity.displayName = roomMember.displayName ?: ""
|
||||
userEntity.avatarUrl = roomMember.avatarUrl ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2019 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.matrix.android.internal.session.user
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.internal.database.model.UserEntity
|
||||
|
||||
internal object UserEntityFactory {
|
||||
|
||||
fun createOrNull(event: Event): UserEntity? {
|
||||
if (event.type != EventType.STATE_ROOM_MEMBER) {
|
||||
return null
|
||||
}
|
||||
val roomMember = event.content.toModel<RoomMember>() ?: return null
|
||||
return UserEntity(event.stateKey ?: "",
|
||||
roomMember.displayName ?: "",
|
||||
roomMember.avatarUrl ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 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.matrix.android.internal.session.user
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.types
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.TaskThread
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import io.realm.Sort
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class UserEntityUpdater @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
private val updateUserTask: UpdateUserTask,
|
||||
private val taskExecutor: TaskExecutor)
|
||||
: RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query<EventEntity> {
|
||||
EventEntity
|
||||
.types(it, listOf(EventType.STATE_ROOM_MEMBER))
|
||||
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
|
||||
.distinct(EventEntityFields.STATE_KEY)
|
||||
}
|
||||
|
||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
val roomMembersEvents = changeSet.insertions
|
||||
.asSequence()
|
||||
.mapNotNull { results[it]?.eventId }
|
||||
.toList()
|
||||
|
||||
val taskParams = UpdateUserTask.Params(roomMembersEvents)
|
||||
updateUserTask
|
||||
.configureWith(taskParams)
|
||||
.executeOn(TaskThread.IO)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
}
|
@ -26,7 +26,4 @@ internal abstract class UserModule {
|
||||
@Binds
|
||||
abstract fun bindUserService(userService: DefaultUserService): UserService
|
||||
|
||||
@Binds
|
||||
abstract fun bindUpdateUserTask(updateUserTask: DefaultUpdateUserTask): UpdateUserTask
|
||||
|
||||
}
|
Reference in New Issue
Block a user