Compare commits

..

2 Commits

Author SHA1 Message Date
ganfra 51a4c93676 Read markers: continue working on ui 2019-08-23 16:54:32 +02:00
ganfra d8f449388c Read marker: start working on it (no UI) 2019-08-20 18:30:24 +02:00
166 changed files with 1915 additions and 3796 deletions

View File

@ -1,26 +1,3 @@
Changes in RiotX 0.5.0 (2019-XX-XX)
===================================================

Features:
-

Improvements:
- Reduce default release build log level, and lab option to enable more logs.

Other changes:
-

Bugfix:
- Fix crash due to missing informationData (#535)
- Progress in initial sync dialog is decreasing for a step and should not (#532)

Translations:
-

Build:
- Fix issue with version name (#533)
- Fix rendering issue of accepted third party invitation event

Changes in RiotX 0.4.0 (2019-XX-XX) Changes in RiotX 0.4.0 (2019-XX-XX)
=================================================== ===================================================


@ -28,16 +5,19 @@ Features:
- Display read receipts in timeline (#81) - Display read receipts in timeline (#81)


Improvements: Improvements:
- Reactions: Reinstate the ability to react with non-unicode keys (#307) -

Other changes:
-


Bugfix: Bugfix:
- Fix text diff linebreak display (#441) -
- Date change message repeats for each redaction until a normal message (#358)
- Slide-in reply icon is distorted (#423) Translations:
- Regression / e2e replies not encrypted -
- Some video won't play
- Privacy: remove log of notifiable event (#519) Build:
- Fix crash with EmojiCompat (#530) -


Changes in RiotX 0.3.0 (2019-08-08) Changes in RiotX 0.3.0 (2019-08-08)
=================================================== ===================================================

View File

@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m




vector.debugPrivateData=false vector.debugPrivateData=false
vector.httpLogLevel=NONE vector.httpLogLevel=HEADERS


# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above # Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above
#vector.debugPrivateData=true #vector.debugPrivateData=true

View File

@ -21,7 +21,7 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlin.random.Random import java.util.*


internal class CryptoStoreHelper { internal class CryptoStoreHelper {


@ -35,7 +35,7 @@ internal class CryptoStoreHelper {
} }


fun createCredential() = Credentials( fun createCredential() = Credentials(
userId = "userId_" + Random.nextInt(), userId = "userId_" + Random().nextInt(),
homeServer = "http://matrix.org", homeServer = "http://matrix.org",
accessToken = "access_token", accessToken = "access_token",
refreshToken = null, refreshToken = null,

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.comparators package im.vector.matrix.android.api.comparators


import im.vector.matrix.android.api.interfaces.DatedObject import im.vector.matrix.android.api.interfaces.DatedObject
import java.util.*


object DatedObjectComparators { object DatedObjectComparators {



View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.api.extensions
import im.vector.matrix.android.api.comparators.DatedObjectComparators import im.vector.matrix.android.api.comparators.DatedObjectComparators
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import java.util.Collections import java.util.*


/* ========================================================================================== /* ==========================================================================================
* MXDeviceInfo * MXDeviceInfo

View File

@ -18,17 +18,18 @@ package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import timber.log.Timber import timber.log.Timber
import java.util.regex.Pattern


private val regex = Regex("^(==|<=|>=|<|>)?(\\d*)$") private val regex = Pattern.compile("^(==|<=|>=|<|>)?(\\d*)$")


class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_count) { class RoomMemberCountCondition(val `is`: String) : Condition(Kind.room_member_count) {


override fun isSatisfied(conditionResolver: ConditionResolver): Boolean { override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveRoomMemberCountCondition(this) return conditionResolver.resolveRoomMemberCountCondition(this)
} }


override fun technicalDescription(): String { override fun technicalDescription(): String {
return "Room member count is $iz" return "Room member count is $`is`"
} }


fun isSatisfied(event: Event, session: RoomService?): Boolean { fun isSatisfied(event: Event, session: RoomService?): Boolean {
@ -55,9 +56,12 @@ class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_coun
*/ */
private fun parseIsField(): Pair<String?, Int>? { private fun parseIsField(): Pair<String?, Int>? {
try { try {
val match = regex.find(iz) ?: return null val match = regex.matcher(`is`)
val (prefix, count) = match.destructured if (match.find()) {
return prefix to count.toInt() val prefix = match.group(1)
val count = match.group(2).toInt()
return prefix to count
}
} catch (t: Throwable) { } catch (t: Throwable) {
Timber.d(t) Timber.d(t)
} }

View File

@ -20,10 +20,10 @@ import androidx.lifecycle.LiveData


interface InitialSyncProgressService { interface InitialSyncProgressService {


fun getInitialSyncProgressStatus() : LiveData<Status?> fun getLiveStatus() : LiveData<Status?>


data class Status( data class Status(
@StringRes val statusText: Int, @StringRes val statusText: Int?,
val percentProgress: Int = 0 val percentProgress: Int = 0
) )
} }

View File

@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.pushers


import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import java.util.UUID import java.util.*




interface PushersService { interface PushersService {

View File

@ -35,9 +35,14 @@ data class RoomSummary(
val highlightCount: Int = 0, val highlightCount: Int = 0,
val tags: List<RoomTag> = emptyList(), val tags: List<RoomTag> = emptyList(),
val membership: Membership = Membership.NONE, val membership: Membership = Membership.NONE,
val versioningState: VersioningState = VersioningState.NONE val versioningState: VersioningState = VersioningState.NONE,
val readMarkerId: String? = null
) { ) {


val isVersioned: Boolean val isVersioned: Boolean
get() = versioningState != VersioningState.NONE get() = versioningState != VersioningState.NONE

val hasNewMessages: Boolean
get() = notificationCount != 0
} }


View File

@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.PowerLevels
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.internal.auth.data.ThreePidMedium import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import java.util.*


/** /**
* Parameter to create a room, with facilities functions to configure it * Parameter to create a room, with facilities functions to configure it
@ -132,7 +133,7 @@ class CreateRoomParams {
) )


if (null == initialStates) { if (null == initialStates) {
initialStates = mutableListOf(algoEvent) initialStates = Arrays.asList<Event>(algoEvent)
} else { } else {
initialStates!!.add(algoEvent) initialStates!!.add(algoEvent)
} }
@ -165,7 +166,7 @@ class CreateRoomParams {
content = contentMap.toContent()) content = contentMap.toContent())


if (null == initialStates) { if (null == initialStates) {
initialStates = mutableListOf(historyVisibilityEvent) initialStates = Arrays.asList<Event>(historyVisibilityEvent)
} else { } else {
initialStates!!.add(historyVisibilityEvent) initialStates!!.add(historyVisibilityEvent)
} }

View File

@ -0,0 +1,25 @@
/*
* 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.session.room.read

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class FullyReadContent(
@Json(name = "event_id") val eventId: String
)

View File

@ -42,5 +42,10 @@ interface ReadService {


fun isEventRead(eventId: String): Boolean fun isEventRead(eventId: String): Boolean


/**
* Returns a nullable read marker for the room.
*/
fun getReadMarkerLive(): LiveData<String?>

fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
} }

View File

@ -32,6 +32,8 @@ interface Timeline {


var listener: Listener? var listener: Listener?


val isLive: Boolean

/** /**
* This should be called before any other method after creating the timeline. It ensures the underlying database is open * This should be called before any other method after creating the timeline. It ensures the underlying database is open
*/ */
@ -42,6 +44,10 @@ interface Timeline {
*/ */
fun dispose() fun dispose()



fun restartWithEventId(eventId: String)


/** /**
* Check if the timeline can be enriched by paginating. * Check if the timeline can be enriched by paginating.
* @param the direction to check in * @param the direction to check in
@ -49,6 +55,7 @@ interface Timeline {
*/ */
fun hasMoreToLoad(direction: Direction): Boolean fun hasMoreToLoad(direction: Direction): Boolean



/** /**
* This is the main method to enrich the timeline with new data. * This is the main method to enrich the timeline with new data.
* It will call the onUpdated method from [Listener] when the data will be processed. * It will call the onUpdated method from [Listener] when the data will be processed.
@ -60,6 +67,13 @@ interface Timeline {


fun failedToDeliverEventCount(): Int fun failedToDeliverEventCount(): Int


fun getIndexOfEvent(eventId: String?): Int?

fun getTimelineEventAtIndex(index: Int): TimelineEvent?

fun getTimelineEventWithId(eventId: String?): TimelineEvent?


interface Listener { interface Listener {
/** /**
* Call when the timeline has been updated through pagination or sync. * Call when the timeline has been updated through pagination or sync.

View File

@ -39,7 +39,8 @@ data class TimelineEvent(
val isUniqueDisplayName: Boolean, val isUniqueDisplayName: Boolean,
val senderAvatar: String?, val senderAvatar: String?,
val annotations: EventAnnotationsSummary? = null, val annotations: EventAnnotationsSummary? = null,
val readReceipts: List<ReadReceipt> = emptyList() val readReceipts: List<ReadReceipt> = emptyList(),
val hasReadMarker: Boolean = false
) { ) {


val metadata = HashMap<String, Any>() val metadata = HashMap<String, Any>()

View File

@ -27,7 +27,7 @@ import java.math.BigInteger
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.KeyStore import java.security.KeyStore
import java.security.SecureRandom import java.security.SecureRandom
import java.util.Calendar import java.util.*
import javax.crypto.* import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
@ -479,7 +479,12 @@ object SecretStoringUtils {
val output = Cipher.getInstance(RSA_MODE) val output = Cipher.getInstance(RSA_MODE)
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey) output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey)


return CipherInputStream(encrypted, output).use { it.readBytes() } val bos = ByteArrayOutputStream()
CipherInputStream(encrypted, output).use {
it.copyTo(bos)
}

return bos.toByteArray()
} }


private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> { private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> {
@ -490,7 +495,14 @@ object SecretStoringUtils {
val iv = ByteArray(ivSize) val iv = ByteArray(ivSize)
bis.read(iv, 0, ivSize) bis.read(iv, 0, ivSize)


val encrypted = bis.readBytes()
val bos = ByteArrayOutputStream()
var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
return Pair(iv, encrypted) return Pair(iv, encrypted)
} }


@ -518,7 +530,14 @@ object SecretStoringUtils {
val iv = ByteArray(ivSize) val iv = ByteArray(ivSize)
bis.read(iv) bis.read(iv)


val encrypted = bis.readBytes() val bos = ByteArrayOutputStream()

var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
return Triple(encryptedKey, iv, encrypted) return Triple(encryptedKey, iv, encrypted)
} }


@ -560,7 +579,14 @@ object SecretStoringUtils {
val iv = ByteArray(ivSize) val iv = ByteArray(ivSize)
bis.read(iv) bis.read(iv)


val encrypted = bis.readBytes() val bos = ByteArrayOutputStream()

var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
return Triple(salt, iv, encrypted) return Triple(salt, iv, encrypted)
} }
} }

View File

@ -93,7 +93,7 @@ import kotlin.math.max
* Specially, it tracks all room membership changes events in order to do keys updates. * Specially, it tracks all room membership changes events in order to do keys updates.
*/ */
@SessionScope @SessionScope
internal class DefaultCryptoService @Inject constructor( internal class CryptoManager @Inject constructor(
// Olm Manager // Olm Manager
private val olmManager: OlmManager, private val olmManager: OlmManager,
// The credentials, // The credentials,
@ -1067,6 +1067,6 @@ internal class DefaultCryptoService @Inject constructor(
* ========================================================================================== */ * ========================================================================================== */


override fun toString(): String { override fun toString(): String {
return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")" return "CryptoManager of " + credentials.userId + " (" + credentials.deviceId + ")"
} }
} }

View File

@ -105,7 +105,7 @@ internal abstract class CryptoModule {
} }


@Binds @Binds
abstract fun bindCryptoService(cryptoService: DefaultCryptoService): CryptoService abstract fun bindCryptoService(cryptoManager: CryptoManager): CryptoService


@Binds @Binds
abstract fun bindDeleteDeviceTask(deleteDeviceTask: DefaultDeleteDeviceTask): DeleteDeviceTask abstract fun bindDeleteDeviceTask(deleteDeviceTask: DefaultDeleteDeviceTask): DeleteDeviceTask

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto


import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import java.util.*
import javax.inject.Inject import javax.inject.Inject


internal class ObjectSigner @Inject constructor(private val credentials: Credentials, internal class ObjectSigner @Inject constructor(private val credentials: Credentials,

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.crypto.model.event.OlmPayloadContent
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.util.convertFromUTF8 import im.vector.matrix.android.internal.util.convertFromUTF8
import timber.log.Timber import timber.log.Timber
import java.util.*


internal class MXOlmDecryption( internal class MXOlmDecryption(
// The olm device interface // The olm device interface
@ -157,14 +158,33 @@ internal class MXOlmDecryption(
* @return payload, if decrypted successfully. * @return payload, if decrypted successfully.
*/ */
private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? { private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) ?: emptySet() val sessionIdsSet = olmDevice.getSessionIds(theirDeviceIdentityKey)


val messageBody = message["body"] as? String ?: return null val sessionIds: List<String>
val messageType = when (val typeAsVoid = message["type"]) {
is Double -> typeAsVoid.toInt() if (null == sessionIdsSet) {
is Int -> typeAsVoid sessionIds = ArrayList()
is Long -> typeAsVoid.toInt() } else {
else -> return null sessionIds = ArrayList(sessionIdsSet)
}

val messageBody = message["body"] as? String
var messageType: Int? = null

val typeAsVoid = message["type"]

if (null != typeAsVoid) {
if (typeAsVoid is Double) {
messageType = typeAsVoid.toInt()
} else if (typeAsVoid is Int) {
messageType = typeAsVoid
} else if (typeAsVoid is Long) {
messageType = typeAsVoid.toInt()
}
}

if (null == messageBody || null == messageType) {
return null
} }


// Try each session in turn // Try each session in turn

View File

@ -26,6 +26,7 @@ import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import java.util.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -58,7 +59,8 @@ object MXEncryptedAttachments {
// Half of the IV is random, the lower order bits are zeroed // Half of the IV is random, the lower order bits are zeroed
// such that the counter never wraps. // such that the counter never wraps.
// See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75 // See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75
val initVectorBytes = ByteArray(16) { 0.toByte() } val initVectorBytes = ByteArray(16)
Arrays.fill(initVectorBytes, 0.toByte())


val ivRandomPart = ByteArray(8) val ivRandomPart = ByteArray(8)
secureRandom.nextBytes(ivRandomPart) secureRandom.nextBytes(ivRandomPart)
@ -113,7 +115,7 @@ object MXEncryptedAttachments {
encryptedByteArray = outStream.toByteArray() encryptedByteArray = outStream.toByteArray()
) )


Timber.v("Encrypt in ${System.currentTimeMillis() - t0} ms") Timber.v("Encrypt in " + (System.currentTimeMillis() - t0) + " ms")
return Try.just(result) return Try.just(result)
} catch (oom: OutOfMemoryError) { } catch (oom: OutOfMemoryError) {
Timber.e(oom, "## encryptAttachment failed") Timber.e(oom, "## encryptAttachment failed")
@ -204,13 +206,13 @@ object MXEncryptedAttachments {
val decryptedStream = ByteArrayInputStream(outStream.toByteArray()) val decryptedStream = ByteArrayInputStream(outStream.toByteArray())
outStream.close() outStream.close()


Timber.v("Decrypt in ${System.currentTimeMillis() - t0} ms") Timber.v("Decrypt in " + (System.currentTimeMillis() - t0) + " ms")


return decryptedStream return decryptedStream
} catch (oom: OutOfMemoryError) { } catch (oom: OutOfMemoryError) {
Timber.e(oom, "## decryptAttachment() : failed ${oom.message}") Timber.e(oom, "## decryptAttachment() : failed " + oom.message)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## decryptAttachment() : failed ${e.message}") Timber.e(e, "## decryptAttachment() : failed " + e.message)
} }


try { try {
@ -226,20 +228,34 @@ object MXEncryptedAttachments {
* Base64 URL conversion methods * Base64 URL conversion methods
*/ */


private fun base64UrlToBase64(base64Url: String): String { private fun base64UrlToBase64(base64Url: String?): String? {
return base64Url.replace('-', '+') var result = base64Url
.replace('_', '/') if (null != result) {
result = result.replace("-".toRegex(), "+")
result = result.replace("_".toRegex(), "/")
} }


private fun base64ToBase64Url(base64: String): String { return result
return base64.replace("\n".toRegex(), "")
.replace("\\+".toRegex(), "-")
.replace('/', '_')
.replace("=", "")
} }


private fun base64ToUnpaddedBase64(base64: String): String { private fun base64ToBase64Url(base64: String?): String? {
return base64.replace("\n".toRegex(), "") var result = base64
.replace("=", "") if (null != result) {
result = result.replace("\n".toRegex(), "")
result = result.replace("\\+".toRegex(), "-")
result = result.replace("/".toRegex(), "_")
result = result.replace("=".toRegex(), "")
}
return result
}

private fun base64ToUnpaddedBase64(base64: String?): String? {
var result = base64
if (null != result) {
result = result.replace("\n".toRegex(), "")
result = result.replace("=".toRegex(), "")
}

return result
} }
} }

View File

@ -66,8 +66,9 @@ import org.matrix.olm.OlmPkEncryption
import org.matrix.olm.OlmPkMessage import org.matrix.olm.OlmPkMessage
import timber.log.Timber import timber.log.Timber
import java.security.InvalidParameterException import java.security.InvalidParameterException
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random import kotlin.collections.HashMap


/** /**
* A KeysBackup class instance manage incremental backup of e2e keys (megolm keys) * A KeysBackup class instance manage incremental backup of e2e keys (megolm keys)
@ -113,6 +114,8 @@ internal class KeysBackup @Inject constructor(
// The backup key being used. // The backup key being used.
private var backupOlmPkEncryption: OlmPkEncryption? = null private var backupOlmPkEncryption: OlmPkEncryption? = null


private val random = Random()

private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null


private var keysBackupStateListener: KeysBackupStateListener? = null private var keysBackupStateListener: KeysBackupStateListener? = null
@ -845,7 +848,7 @@ internal class KeysBackup @Inject constructor(
// Wait between 0 and 10 seconds, to avoid backup requests from // Wait between 0 and 10 seconds, to avoid backup requests from
// different clients hitting the server all at the same time when a // different clients hitting the server all at the same time when a
// new key is sent // new key is sent
val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS) val delayInMs = random.nextInt(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS).toLong()


uiHandler.postDelayed({ backupKeys() }, delayInMs) uiHandler.postDelayed({ backupKeys() }, delayInMs)
} }
@ -1402,7 +1405,7 @@ internal class KeysBackup @Inject constructor(


companion object { companion object {
// Maximum delay in ms in {@link maybeBackupKeys} // Maximum delay in ms in {@link maybeBackupKeys}
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10000


// Maximum number of keys to send at a time to the homeserver. // Maximum number of keys to send at a time to the homeserver.
private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100 private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100

View File

@ -22,7 +22,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.listeners.ProgressListener
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.*
import javax.crypto.Mac import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.xor import kotlin.experimental.xor

View File

@ -20,6 +20,7 @@ import android.os.Handler
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
import timber.log.Timber import timber.log.Timber
import java.util.*


internal class KeysBackupStateManager(private val uiHandler: Handler) { internal class KeysBackupStateManager(private val uiHandler: Handler) {



View File

@ -23,6 +23,7 @@ import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.crypto.model.rest.DeviceKeys import im.vector.matrix.android.internal.crypto.model.rest.DeviceKeys
import java.io.Serializable import java.io.Serializable
import java.util.*


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MXDeviceInfo( data class MXDeviceInfo(

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.model


import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import timber.log.Timber import timber.log.Timber
import java.util.*


data class MXKey( data class MXKey(
/** /**
@ -45,7 +46,11 @@ data class MXKey(
* @return the signed data map * @return the signed data map
*/ */
fun signalableJSONDictionary(): Map<String, Any> { fun signalableJSONDictionary(): Map<String, Any> {
return mapOf("key" to value) val map = HashMap<String, Any>()

map["key"] = value

return map
} }


/** /**

View File

@ -16,6 +16,7 @@


package im.vector.matrix.android.internal.crypto.model package im.vector.matrix.android.internal.crypto.model


import java.util.*


class MXUsersDevicesMap<E> { class MXUsersDevicesMap<E> {


@ -26,7 +27,7 @@ class MXUsersDevicesMap<E> {
* @return the user Ids * @return the user Ids
*/ */
val userIds: List<String> val userIds: List<String>
get() = map.keys.toList() get() = ArrayList(map.keys)


val isEmpty: Boolean val isEmpty: Boolean
get() = map.isEmpty() get() = map.isEmpty()
@ -39,7 +40,7 @@ class MXUsersDevicesMap<E> {
* @return the device ids list * @return the device ids list
*/ */
fun getUserDeviceIds(userId: String?): List<String>? { fun getUserDeviceIds(userId: String?): List<String>? {
return if (!userId.isNullOrBlank() && map.containsKey(userId)) { return if (userId?.isNotBlank() == true && map.containsKey(userId)) {
map[userId]!!.keys.toList() map[userId]!!.keys.toList()
} else null } else null
} }
@ -52,7 +53,7 @@ class MXUsersDevicesMap<E> {
* @return the object * @return the object
*/ */
fun getObject(userId: String?, deviceId: String?): E? { fun getObject(userId: String?, deviceId: String?): E? {
return if (!userId.isNullOrBlank() && !deviceId.isNullOrBlank()) { return if (userId?.isNotBlank() == true && deviceId?.isNotBlank() == true && map.containsKey(userId)) {
map[userId]?.get(deviceId) map[userId]?.get(deviceId)
} else null } else null
} }
@ -66,8 +67,11 @@ class MXUsersDevicesMap<E> {
*/ */
fun setObject(userId: String?, deviceId: String?, o: E?) { fun setObject(userId: String?, deviceId: String?, o: E?) {
if (null != o && userId?.isNotBlank() == true && deviceId?.isNotBlank() == true) { if (null != o && userId?.isNotBlank() == true && deviceId?.isNotBlank() == true) {
val devices = map.getOrPut(userId) { HashMap() } if (map[userId] == null) {
devices[deviceId] = o map[userId] = HashMap()
}

map[userId]?.put(deviceId, o)
} }
} }


@ -78,7 +82,7 @@ class MXUsersDevicesMap<E> {
* @param userId the user id * @param userId the user id
*/ */
fun setObjects(userId: String?, objectsPerDevices: Map<String, E>?) { fun setObjects(userId: String?, objectsPerDevices: Map<String, E>?) {
if (!userId.isNullOrBlank()) { if (userId?.isNotBlank() == true) {
if (null == objectsPerDevices) { if (null == objectsPerDevices) {
map.remove(userId) map.remove(userId)
} else { } else {
@ -93,7 +97,7 @@ class MXUsersDevicesMap<E> {
* @param userId the user id. * @param userId the user id.
*/ */
fun removeUserObjects(userId: String?) { fun removeUserObjects(userId: String?) {
if (!userId.isNullOrBlank()) { if (userId?.isNotBlank() == true) {
map.remove(userId) map.remove(userId)
} }
} }

View File

@ -21,8 +21,8 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceBody import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceBody
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random


internal interface SendToDeviceTask : Task<SendToDeviceTask.Params, Unit> { internal interface SendToDeviceTask : Task<SendToDeviceTask.Params, Unit> {
data class Params( data class Params(
@ -45,7 +45,7 @@ internal class DefaultSendToDeviceTask @Inject constructor(private val cryptoApi
return executeRequest { return executeRequest {
apiCall = cryptoApi.sendToDevice( apiCall = cryptoApi.sendToDevice(
params.eventType, params.eventType,
params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(), params.transactionId ?: Random().nextInt(Integer.MAX_VALUE).toString(),
sendToDeviceBody sendToDeviceBody
) )
} }

View File

@ -44,7 +44,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.HashMap import kotlin.collections.HashMap


@ -161,7 +161,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
cancelTransaction( cancelTransaction(
startReq.transactionID!!, startReq.transactionID!!,
otherUserId!!, otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!, startReq?.fromDevice ?: event.getSenderKey()!!,
CancelCode.UnknownMethod CancelCode.UnknownMethod
) )
} }
@ -388,13 +388,14 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid * This string must be unique for the pair of users performing verification for the duration that the transaction is valid
*/ */
private fun createUniqueIDForTransaction(userId: String, deviceID: String): String { private fun createUniqueIDForTransaction(userId: String, deviceID: String): String {
return buildString { val buff = StringBuffer()
append(credentials.userId).append("|") buff
append(credentials.deviceId).append("|") .append(credentials.userId).append("|")
append(userId).append("|") .append(credentials.deviceId).append("|")
append(deviceID).append("|") .append(userId).append("|")
append(UUID.randomUUID().toString()) .append(deviceID).append("|")
} .append(UUID.randomUUID().toString())
return buff.toString()
} }





View File

@ -23,6 +23,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity
@ -157,7 +158,6 @@ internal fun ChunkEntity.add(roomId: String,
} }
} }



val eventEntity = TimelineEventEntity(localId).also { val eventEntity = TimelineEventEntity(localId).also {
it.root = event.toEntity(roomId).apply { it.root = event.toEntity(roomId).apply {
this.stateIndex = currentStateIndex this.stateIndex = currentStateIndex
@ -169,6 +169,7 @@ internal fun ChunkEntity.add(roomId: String,
it.roomId = roomId it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
it.readReceipts = readReceiptsSummaryEntity it.readReceipts = readReceiptsSummaryEntity
it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
} }
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity) timelineEvents.add(position, eventEntity)

View File

@ -22,12 +22,12 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import java.util.UUID import java.util.*
import javax.inject.Inject import javax.inject.Inject


internal class RoomSummaryMapper @Inject constructor( internal class RoomSummaryMapper @Inject constructor(
val cryptoService: CryptoService, private val cryptoService: CryptoService,
val timelineEventMapper: TimelineEventMapper private val timelineEventMapper: TimelineEventMapper
) { ) {


fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
@ -65,7 +65,8 @@ internal class RoomSummaryMapper @Inject constructor(
notificationCount = roomSummaryEntity.notificationCount, notificationCount = roomSummaryEntity.notificationCount,
tags = tags, tags = tags,
membership = roomSummaryEntity.membership, membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState versioningState = roomSummaryEntity.versioningState,
readMarkerId = roomSummaryEntity.readMarkerId
) )
} }
} }

View File

@ -45,7 +45,8 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
senderAvatar = timelineEventEntity.senderAvatar, senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = readReceipts?.sortedByDescending { readReceipts = readReceipts?.sortedByDescending {
it.originServerTs it.originServerTs
} ?: emptyList() } ?: emptyList(),
hasReadMarker = timelineEventEntity.readMarker?.eventId?.isEmpty() == false
) )
} }



View File

@ -0,0 +1,35 @@
/*
* 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.database.model

import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey

internal open class ReadMarkerEntity(
@PrimaryKey
var roomId: String = "",
var eventId: String = ""
) : RealmObject() {

@LinkingObjects("readMarker")
val timelineEvent: RealmResults<TimelineEventEntity>? = null

companion object

}

View File

@ -35,7 +35,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var otherMemberIds: RealmList<String> = RealmList(), var otherMemberIds: RealmList<String> = RealmList(),
var notificationCount: Int = 0, var notificationCount: Int = 0,
var highlightCount: Int = 0, var highlightCount: Int = 0,
var tags: RealmList<RoomTagEntity> = RealmList() var tags: RealmList<RoomTagEntity> = RealmList(),
var readMarkerId: String? = null
) : RealmObject() { ) : RealmObject() {


private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name

View File

@ -43,6 +43,7 @@ import io.realm.annotations.RealmModule
PushConditionEntity::class, PushConditionEntity::class,
PusherEntity::class, PusherEntity::class,
PusherDataEntity::class, PusherDataEntity::class,
ReadReceiptsSummaryEntity::class ReadReceiptsSummaryEntity::class,
ReadMarkerEntity::class
]) ])
internal class SessionRealmModule internal class SessionRealmModule

View File

@ -31,7 +31,8 @@ internal open class TimelineEventEntity(var localId: Long = 0,
var isUniqueDisplayName: Boolean = false, var isUniqueDisplayName: Boolean = false,
var senderAvatar: String? = null, var senderAvatar: String? = null,
var senderMembershipEvent: EventEntity? = null, var senderMembershipEvent: EventEntity? = null,
var readReceipts: ReadReceiptsSummaryEntity? = null var readReceipts: ReadReceiptsSummaryEntity? = null,
var readMarker: ReadMarkerEntity? = null
) : RealmObject() { ) : RealmObject() {


@LinkingObjects("timelineEvents") @LinkingObjects("timelineEvents")

View File

@ -0,0 +1,37 @@
/*
* 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.database.query

import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where

internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery<ReadMarkerEntity> {
val query = realm.where<ReadMarkerEntity>()
.equalTo(ReadMarkerEntityFields.ROOM_ID, roomId)
if (eventId != null) {
query.equalTo(ReadMarkerEntityFields.EVENT_ID, eventId)
}
return query
}

internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity {
return where(realm, roomId).findFirst()
?: realm.createObject(ReadMarkerEntity::class.java, roomId)
}

View File

@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.kotlin.where import io.realm.kotlin.where


internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> { internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> {
@ -28,6 +29,12 @@ internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, use
.equalTo(ReadReceiptEntityFields.USER_ID, userId) .equalTo(ReadReceiptEntityFields.USER_ID, userId)
} }


internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery<ReadReceiptEntity> {
return realm.where<ReadReceiptEntity>()
.equalTo(ReadReceiptEntityFields.USER_ID, userId)
}


internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity {
return ReadReceiptEntity().apply { return ReadReceiptEntity().apply {
this.primaryKey = "${roomId}_$userId" this.primaryKey = "${roomId}_$userId"

View File

@ -31,6 +31,12 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n
return query return query
} }


internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity {
return where(realm, roomId).findFirst()
?: realm.createObject(RoomSummaryEntity::class.java, roomId)
}


internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> { internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> {
return RoomSummaryEntity.where(realm) return RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) .equalTo(RoomSummaryEntityFields.IS_DIRECT, true)

View File

@ -21,4 +21,4 @@ import javax.inject.Scope
@Scope @Scope
@MustBeDocumented @MustBeDocumented
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class MatrixScope internal annotation class MatrixScope

View File

@ -24,6 +24,7 @@ import java.security.KeyStore
import java.security.MessageDigest import java.security.MessageDigest
import java.security.cert.CertificateException import java.security.cert.CertificateException
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.*
import javax.net.ssl.* import javax.net.ssl.*
import kotlin.experimental.and import kotlin.experimental.and



View File

@ -25,6 +25,7 @@ import java.net.UnknownHostException
import java.security.KeyManagementException import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.security.SecureRandom import java.security.SecureRandom
import java.util.*
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory import javax.net.ssl.SSLSocketFactory
@ -100,16 +101,25 @@ constructor(trustPinned: Array<TrustManager>, acceptedTlsVersions: List<TlsVersi
} }


private fun enableTLSOnSocket(socket: Socket?): Socket? { private fun enableTLSOnSocket(socket: Socket?): Socket? {
if (socket is SSLSocket) { if (socket != null && socket is SSLSocket) {
val supportedProtocols = socket.supportedProtocols.toSet() val sslSocket = socket as SSLSocket?
val filteredEnabledProtocols = enabledProtocols.filter { it in supportedProtocols }


if (filteredEnabledProtocols.isNotEmpty()) { val supportedProtocols = Arrays.asList(*sslSocket!!.supportedProtocols)
val filteredEnabledProtocols = ArrayList<String>()

for (protocol in enabledProtocols) {
if (supportedProtocols.contains(protocol)) {
filteredEnabledProtocols.add(protocol)
}
}

if (!filteredEnabledProtocols.isEmpty()) {
try { try {
socket.enabledProtocols = filteredEnabledProtocols.toTypedArray() sslSocket.enabledProtocols = filteredEnabledProtocols.toTypedArray()
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
} }

} }
} }
return socket return socket

View File

@ -15,7 +15,6 @@
*/ */
package im.vector.matrix.android.internal.session package im.vector.matrix.android.internal.session


import androidx.annotation.StringRes
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.InitialSyncProgressService
@ -26,33 +25,31 @@ import javax.inject.Inject
@SessionScope @SessionScope
class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgressService { class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgressService {


private var status = MutableLiveData<InitialSyncProgressService.Status>() var status = MutableLiveData<InitialSyncProgressService.Status>()


private var rootTask: TaskInfo? = null var rootTask: TaskInfo? = null


override fun getInitialSyncProgressStatus(): LiveData<InitialSyncProgressService.Status?> { override fun getLiveStatus(): LiveData<InitialSyncProgressService.Status?> {
return status return status
} }


fun startTask(@StringRes nameRes: Int, totalProgress: Int, parentWeight: Float = 1f) {
// Create a rootTask, or add a child to the leaf fun startTask(nameRes: Int, totalProgress: Int, parentWeight: Float = 1f) {
if (rootTask == null) { if (rootTask == null) {
rootTask = TaskInfo(nameRes, totalProgress) rootTask = TaskInfo(nameRes, totalProgress)
} else { } else {
val currentLeaf = rootTask!!.leaf() val currentLeaf = rootTask!!.leaf()

val newTask = TaskInfo(nameRes, totalProgress)
val newTask = TaskInfo(nameRes, newTask.parent = currentLeaf
totalProgress, newTask.offset = currentLeaf.currentProgress
currentLeaf,
parentWeight)

currentLeaf.child = newTask currentLeaf.child = newTask
newTask.parentWeight = parentWeight
} }
reportProgress(0) reportProgress(0)
} }


fun reportProgress(progress: Int) { fun reportProgress(progress: Int) {
rootTask?.leaf()?.setProgress(progress) rootTask?.leaf()?.incrementProgress(progress)
} }


fun endTask(nameRes: Int) { fun endTask(nameRes: Int) {
@ -61,7 +58,7 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
//close it //close it
val parent = endedTask.parent val parent = endedTask.parent
parent?.child = null parent?.child = null
parent?.setProgress(endedTask.offset + (endedTask.totalProgress * endedTask.parentWeight).toInt()) parent?.incrementProgress(endedTask.offset + (endedTask.totalProgress * endedTask.parentWeight).toInt())
} }
if (endedTask?.parent == null) { if (endedTask?.parent == null) {
status.postValue(null) status.postValue(null)
@ -74,17 +71,14 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
} }




private inner class TaskInfo(@StringRes var nameRes: Int, inner class TaskInfo(var nameRes: Int,
var totalProgress: Int, var totalProgress: Int) {
var parent: TaskInfo? = null, var parent: TaskInfo? = null
var parentWeight: Float = 1f,
var offset: Int = parent?.currentProgress ?: 0) {
var child: TaskInfo? = null var child: TaskInfo? = null
var parentWeight: Float = 1f
var currentProgress: Int = 0 var currentProgress: Int = 0
var offset: Int = 0


/**
* Get the further child
*/
fun leaf(): TaskInfo { fun leaf(): TaskInfo {
var last = this var last = this
while (last.child != null) { while (last.child != null) {
@ -93,27 +87,26 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
return last return last
} }


/** fun incrementProgress(progress: Int) {
* Set progress of the parent if any (which will post value), or post the value
*/
fun setProgress(progress: Int) {
currentProgress = progress currentProgress = progress
// val newProgress = Math.min(currentProgress + progress, totalProgress) // val newProgress = Math.min(currentProgress + progress, totalProgress)
parent?.let { parent?.let {
val parentProgress = (currentProgress * parentWeight).toInt() val parentProgress = (currentProgress * parentWeight).toInt()
it.setProgress(offset + parentProgress) it.incrementProgress(offset + parentProgress)
} ?: run { }
Timber.e("--- ${leaf().nameRes}: $currentProgress") if (parent == null) {
Timber.e("--- ${leaf().nameRes}: ${currentProgress}")
status.postValue( status.postValue(
InitialSyncProgressService.Status(leaf().nameRes, currentProgress) InitialSyncProgressService.Status(leaf().nameRes, currentProgress)
) )
} }
} }
} }

} }


inline fun <T> reportSubtask(reporter: DefaultInitialSyncProgressService?, inline fun <T> reportSubtask(reporter: DefaultInitialSyncProgressService?,
@StringRes nameRes: Int, nameRes: Int,
totalProgress: Int, totalProgress: Int,
parentWeight: Float = 1f, parentWeight: Float = 1f,
block: () -> T): T { block: () -> T): T {
@ -128,11 +121,11 @@ inline fun <K, V, R> Map<out K, V>.mapWithProgress(reporter: DefaultInitialSyncP
taskId: Int, taskId: Int,
weight: Float, weight: Float,
transform: (Map.Entry<K, V>) -> R): List<R> { transform: (Map.Entry<K, V>) -> R): List<R> {
val total = count().toFloat() val total = count()
var current = 0 var current = 0
reporter?.startTask(taskId, 100, weight) reporter?.startTask(taskId, 100, weight)
return map { return this.map {
reporter?.reportProgress((current / total * 100).toInt()) reporter?.reportProgress((current / total.toFloat() * 100).toInt())
current++ current++
transform.invoke(it) transform.invoke(it)
}.also { }.also {

View File

@ -40,7 +40,7 @@ import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.util.MatrixCallbackDelegate import im.vector.matrix.android.api.util.MatrixCallbackDelegate
import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.session.sync.job.SyncWorker
@ -63,7 +63,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val signOutService: Lazy<SignOutService>, private val signOutService: Lazy<SignOutService>,
private val pushRuleService: Lazy<PushRuleService>, private val pushRuleService: Lazy<PushRuleService>,
private val pushersService: Lazy<PushersService>, private val pushersService: Lazy<PushersService>,
private val cryptoService: Lazy<DefaultCryptoService>, private val cryptoService: Lazy<CryptoManager>,
private val fileService: Lazy<FileService>, private val fileService: Lazy<FileService>,
private val syncThreadProvider: Provider<SyncThread>, private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,

View File

@ -36,9 +36,7 @@ import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.room.DefaultRoomFactory
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
import im.vector.matrix.android.internal.session.room.RoomFactory
import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver
import im.vector.matrix.android.internal.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver

View File

@ -21,4 +21,4 @@ import javax.inject.Scope
@Scope @Scope
@MustBeDocumented @MustBeDocumented
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class SessionScope internal annotation class SessionScope

View File

@ -30,8 +30,9 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
private val listeners = mutableMapOf<String, MutableList<ContentUploadStateTracker.UpdateListener>>() private val listeners = mutableMapOf<String, MutableList<ContentUploadStateTracker.UpdateListener>>()


override fun track(key: String, updateListener: ContentUploadStateTracker.UpdateListener) { override fun track(key: String, updateListener: ContentUploadStateTracker.UpdateListener) {
val listeners = listeners.getOrPut(key) { ArrayList() } val listeners = listeners[key] ?: ArrayList()
listeners.add(updateListener) listeners.add(updateListener)
this.listeners[key] = listeners
val currentState = states[key] ?: ContentUploadStateTracker.State.Idle val currentState = states[key] ?: ContentUploadStateTracker.State.Idle
mainHandler.post { updateListener.onUpdate(currentState) } mainHandler.post { updateListener.onUpdate(currentState) }
} }

View File

@ -32,7 +32,7 @@ import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil
import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import java.util.UUID import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject



View File

@ -28,8 +28,10 @@ import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.find 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.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
@ -93,6 +95,15 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private
return isEventRead return isEventRead
} }


override fun getReadMarkerLive(): LiveData<String?> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
ReadMarkerEntity.where(realm, roomId)
}
return Transformations.map(liveRealmData) { results ->
results.firstOrNull()?.eventId
}
}

override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> { override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> {
val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm -> val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm ->
ReadReceiptsSummaryEntity.where(realm, eventId) ReadReceiptsSummaryEntity.where(realm, eventId)

View File

@ -18,19 +18,25 @@ package im.vector.matrix.android.internal.session.room.read


import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity 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.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.find 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.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest 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.RoomAPI
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.sync.RoomFullyReadHandler
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject


@ -49,7 +55,8 @@ private const val READ_RECEIPT = "m.read"


internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI, internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI,
private val credentials: Credentials, private val credentials: Credentials,
private val monarchy: Monarchy private val monarchy: Monarchy,
private val roomFullyReadHandler: RoomFullyReadHandler
) : SetReadMarkersTask { ) : SetReadMarkersTask {


override suspend fun execute(params: SetReadMarkersTask.Params) { override suspend fun execute(params: SetReadMarkersTask.Params) {
@ -57,6 +64,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
val fullyReadEventId: String? val fullyReadEventId: String?
val readReceiptEventId: String? val readReceiptEventId: String?


Timber.v("Execute set read marker with params: $params")
if (params.markAllAsRead) { if (params.markAllAsRead) {
val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId
@ -68,16 +76,16 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
readReceiptEventId = params.readReceiptEventId readReceiptEventId = params.readReceiptEventId
} }


if (fullyReadEventId != null) { if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) {
if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) { if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) {
Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") Timber.w("Can't set read marker for local event ${params.fullyReadEventId}")
} else { } else {
updateReadMarker(params.roomId, fullyReadEventId)
markers[READ_MARKER] = fullyReadEventId markers[READ_MARKER] = fullyReadEventId
} }
} }
if (readReceiptEventId != null if (readReceiptEventId != null
&& !isEventRead(params.roomId, readReceiptEventId)) { && !isEventRead(params.roomId, readReceiptEventId)) {

if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) {
Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}") Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}")
} else { } else {
@ -93,12 +101,30 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
} }
} }


private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) {
monarchy.writeAsync { realm -> private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId).findFirst()
val readMarkerEvent = readMarkerEntity?.timelineEvent?.firstOrNull()
val eventToCheck = TimelineEventEntity.where(realm, eventId = fullyReadEventId).findFirst()
val readReceiptIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE
val eventToCheckIndex = eventToCheck?.root?.displayIndex ?: Int.MIN_VALUE
eventToCheckIndex > readReceiptIndex
}
}

private suspend fun updateReadMarker(roomId: String, eventId: String) {
monarchy.awaitTransaction { realm ->
roomFullyReadHandler.handle(realm, roomId, FullyReadContent(eventId))
}
}

private suspend fun updateNotificationCountIfNecessary(roomId: String, eventId: String) {
monarchy.awaitTransaction { realm ->
val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId
if (isLatestReceived) { if (isLatestReceived) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
?: return@writeAsync ?: return@awaitTransaction
roomSummary.notificationCount = 0 roomSummary.notificationCount = 0
roomSummary.highlightCount = 0 roomSummary.highlightCount = 0
} }
@ -106,19 +132,17 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
} }


private fun isEventRead(roomId: String, eventId: String): Boolean { private fun isEventRead(roomId: String, eventId: String): Boolean {
var isEventRead = false return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
monarchy.doWithRealm { val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst()
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() ?: return false
?: return@doWithRealm val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) ?: return false
?: return@doWithRealm
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
?: Int.MIN_VALUE ?: Int.MIN_VALUE
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
?: Int.MAX_VALUE ?: Int.MAX_VALUE
isEventRead = eventToCheckIndex <= readReceiptIndex eventToCheckIndex <= readReceiptIndex
} }
return isEventRead
} }


} }

View File

@ -69,11 +69,15 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
.also { .also {
saveLocalEcho(it) saveLocalEcho(it)
} }
val sendRelationWork = createSendEventWork(event, true) val sendRelationWork = createSendRelationWork(event)
TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork) TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork)
return CancelableWork(context, sendRelationWork.id) return CancelableWork(context, sendRelationWork.id)
} }


private fun createSendRelationWork(event: Event): OneTimeWorkRequest {
return createSendEventWork(event)
}

override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ { override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ {


val params = FindReactionEventForUndoTask.Params( val params = FindReactionEventForUndoTask.Params(
@ -130,42 +134,42 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
.also { .also {
saveLocalEcho(it) saveLocalEcho(it)
} }
return if (cryptoService.isRoomEncrypted(roomId)) { if (cryptoService.isRoomEncrypted(roomId)) {
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
val workRequest = createSendEventWork(event, false) val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
CancelableWork(context, encryptWork.id) return CancelableWork(context, encryptWork.id)


} else { } else {
val workRequest = createSendEventWork(event, true) val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
CancelableWork(context, workRequest.id) return CancelableWork(context, workRequest.id)
} }


} }


override fun editReply(replyToEdit: TimelineEvent, override fun editReply(replyToEdit: TimelineEvent,
originalTimelineEvent: TimelineEvent, originalEvent: TimelineEvent,
newBodyText: String, newBodyText: String,
compatibilityBodyText: String): Cancelable { compatibilityBodyText: String): Cancelable {
val event = eventFactory val event = eventFactory
.createReplaceTextOfReply(roomId, .createReplaceTextOfReply(roomId,
replyToEdit, replyToEdit,
originalTimelineEvent, originalEvent,
newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText) newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
.also { .also {
saveLocalEcho(it) saveLocalEcho(it)
} }
return if (cryptoService.isRoomEncrypted(roomId)) { if (cryptoService.isRoomEncrypted(roomId)) {
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
val workRequest = createSendEventWork(event, false) val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
CancelableWork(context, encryptWork.id) return CancelableWork(context, encryptWork.id)


} else { } else {
val workRequest = createSendEventWork(event, true) val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
CancelableWork(context, workRequest.id) return CancelableWork(context, workRequest.id)
} }
} }


@ -183,16 +187,16 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
saveLocalEcho(it) saveLocalEcho(it)
} ?: return null } ?: return null


return if (cryptoService.isRoomEncrypted(roomId)) { if (cryptoService.isRoomEncrypted(roomId)) {
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
val workRequest = createSendEventWork(event, false) val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
CancelableWork(context, encryptWork.id) return CancelableWork(context, encryptWork.id)


} else { } else {
val workRequest = createSendEventWork(event, true) val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
CancelableWork(context, workRequest.id) return CancelableWork(context, workRequest.id)
} }


} }
@ -204,10 +208,10 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
return TimelineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true) return TimelineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true)
} }


private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { private fun createSendEventWork(event: Event): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, true)
} }


override fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary> { override fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary> {

View File

@ -38,7 +38,7 @@ import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.util.StringProvider import im.vector.matrix.android.internal.util.StringProvider
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
import java.util.UUID import java.util.*
import javax.inject.Inject import javax.inject.Inject


/** /**
@ -304,22 +304,17 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
} }


private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String { private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {
return buildString {
append("> <")
append(originalSenderId)
append(">")

val lines = body.text.split("\n") val lines = body.text.split("\n")
val replyFallback = StringBuffer("> <$originalSenderId>")
lines.forEachIndexed { index, s -> lines.forEachIndexed { index, s ->
if (index == 0) { if (index == 0) {
append(" $s") replyFallback.append(" $s")
} else { } else {
append("\n> $s") replyFallback.append("\n> $s")
} }
} }
append("\n\n") replyFallback.append("\n\n").append(newBodyText)
append(newBodyText) return replyFallback.toString()
}
} }


/** /**

View File

@ -27,33 +27,15 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.model.ChunkEntityFields import im.vector.matrix.android.internal.database.query.*
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
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.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.FilterContent
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
import im.vector.matrix.android.internal.database.query.findIncludingEvent
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.database.query.whereInRoom
import im.vector.matrix.android.internal.task.TaskConstraints import im.vector.matrix.android.internal.task.TaskConstraints
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.Debouncer import im.vector.matrix.android.internal.util.Debouncer
import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createBackgroundHandler
import im.vector.matrix.android.internal.util.createUIHandler import im.vector.matrix.android.internal.util.createUIHandler
import io.realm.OrderedCollectionChangeSet import io.realm.*
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -67,7 +49,7 @@ private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE


internal class DefaultTimeline( internal class DefaultTimeline(
private val roomId: String, private val roomId: String,
private val initialEventId: String? = null, private var initialEventId: String? = null,
private val realmConfiguration: RealmConfiguration, private val realmConfiguration: RealmConfiguration,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask, private val contextOfEventTask: GetContextOfEventTask,
@ -75,8 +57,9 @@ internal class DefaultTimeline(
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val settings: TimelineSettings, private val settings: TimelineSettings,
private val hiddenReadReceipts: TimelineHiddenReadReceipts private val hiddenReadReceipts: TimelineHiddenReadReceipts,
) : Timeline, TimelineHiddenReadReceipts.Delegate { private val hiddenReadMarker: TimelineHiddenReadMarker
) : Timeline, TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadMarker.Delegate {


private companion object { private companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
@ -104,7 +87,6 @@ internal class DefaultTimeline(


private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private val isLive = initialEventId == null
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList()) private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
private val backwardsPaginationState = AtomicReference(PaginationState()) private val backwardsPaginationState = AtomicReference(PaginationState())
@ -112,6 +94,9 @@ internal class DefaultTimeline(


private val timelineID = UUID.randomUUID().toString() private val timelineID = UUID.randomUUID().toString()


override val isLive
get() = initialEventId == null

private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService)


private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<TimelineEventEntity>> { results, changeSet -> private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<TimelineEventEntity>> { results, changeSet ->
@ -120,10 +105,7 @@ internal class DefaultTimeline(
} else { } else {
// If changeSet has deletion we are having a gap, so we clear everything // If changeSet has deletion we are having a gap, so we clear everything
if (changeSet.deletionRanges.isNotEmpty()) { if (changeSet.deletionRanges.isNotEmpty()) {
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN clearAllValues()
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
builtEvents.clear()
builtEventsIdMap.clear()
} }
changeSet.insertionRanges.forEach { range -> changeSet.insertionRanges.forEach { range ->
val (startDisplayIndex, direction) = if (range.startIndex == 0) { val (startDisplayIndex, direction) = if (range.startIndex == 0) {
@ -149,13 +131,9 @@ internal class DefaultTimeline(
changeSet.changes.forEach { index -> changeSet.changes.forEach { index ->
val eventEntity = results[index] val eventEntity = results[index]
eventEntity?.eventId?.let { eventId -> eventEntity?.eventId?.let { eventId ->
builtEventsIdMap[eventId]?.let { builtIndex -> hasChanged = rebuildEvent(eventId) {
//Update an existing event buildTimelineEvent(eventEntity)
builtEvents[builtIndex]?.let { te -> } || hasChanged
builtEvents[builtIndex] = buildTimelineEvent(eventEntity)
hasChanged = true
}
}
} }
} }
if (hasChanged) postSnapshot() if (hasChanged) postSnapshot()
@ -163,23 +141,17 @@ internal class DefaultTimeline(
} }


private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet -> private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet ->

var hasChange = false var hasChange = false


(changeSet.insertions + changeSet.changes).forEach { (changeSet.insertions + changeSet.changes).forEach {
val eventRelations = collection[it] val eventRelations = collection[it]
if (eventRelations != null) { if (eventRelations != null) {
builtEventsIdMap[eventRelations.eventId]?.let { builtIndex -> hasChange = rebuildEvent(eventRelations.eventId) { te ->
//Update the relation of existing event te.copy(annotations = eventRelations.asDomain())
builtEvents[builtIndex]?.let { te -> } || hasChange
builtEvents[builtIndex] = te.copy(annotations = eventRelations.asDomain())
hasChange = true
} }
} }
} if (hasChange) postSnapshot()
}
if (hasChange)
postSnapshot()
} }




@ -240,7 +212,7 @@ internal class DefaultTimeline(
if (settings.buildReadReceipts) { if (settings.buildReadReceipts) {
hiddenReadReceipts.start(realm, liveEvents, this) hiddenReadReceipts.start(realm, liveEvents, this)
} }

hiddenReadMarker.start(realm, liveEvents, this)
isReady.set(true) isReady.set(true)
} }
} }
@ -255,9 +227,11 @@ internal class DefaultTimeline(
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
eventRelations.removeAllChangeListeners() eventRelations.removeAllChangeListeners()
liveEvents.removeAllChangeListeners() liveEvents.removeAllChangeListeners()
hiddenReadMarker.dispose()
if (settings.buildReadReceipts) { if (settings.buildReadReceipts) {
hiddenReadReceipts.dispose() hiddenReadReceipts.dispose()
} }
clearAllValues()
backgroundRealm.getAndSet(null).also { backgroundRealm.getAndSet(null).also {
it.close() it.close()
} }
@ -265,6 +239,27 @@ internal class DefaultTimeline(
} }
} }


override fun restartWithEventId(eventId: String) {
dispose()
initialEventId = eventId
start()
postSnapshot()
}

override fun getIndexOfEvent(eventId: String?): Int? {
return builtEventsIdMap[eventId]
}

override fun getTimelineEventAtIndex(index: Int): TimelineEvent? {
return builtEvents.getOrNull(index)
}

override fun getTimelineEventWithId(eventId: String?): TimelineEvent? {
return builtEventsIdMap[eventId]?.let {
getTimelineEventAtIndex(it)
}
}

override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
return hasMoreInCache(direction) || !hasReachedEnd(direction) return hasMoreInCache(direction) || !hasReachedEnd(direction)
} }
@ -272,21 +267,39 @@ internal class DefaultTimeline(
// TimelineHiddenReadReceipts.Delegate // TimelineHiddenReadReceipts.Delegate


override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean { override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
return builtEventsIdMap[eventId]?.let { builtIndex -> return rebuildEvent(eventId) { te ->
//Update the relation of existing event te.copy(readReceipts = readReceipts)
builtEvents[builtIndex]?.let { te ->
builtEvents[builtIndex] = te.copy(readReceipts = readReceipts)
true
} }
} ?: false
} }


override fun onReadReceiptsUpdated() { override fun onReadReceiptsUpdated() {
postSnapshot() postSnapshot()
} }


// TimelineHiddenReadMarker.Delegate

override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean {
return rebuildEvent(eventId) { te ->
te.copy(hasReadMarker = hasReadMarker)
}
}

override fun onReadMarkerUpdated() {
postSnapshot()
}

// Private methods ***************************************************************************** // Private methods *****************************************************************************


private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
return builtEventsIdMap[eventId]?.let { builtIndex ->
//Update the relation of existing event
builtEvents[builtIndex]?.let { te ->
builtEvents[builtIndex] = builder(te)
true
}
} ?: false
}

private fun hasMoreInCache(direction: Timeline.Direction): Boolean { private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
return Realm.getInstance(realmConfiguration).use { localRealm -> return Realm.getInstance(realmConfiguration).use { localRealm ->
val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction) val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction)
@ -395,8 +408,9 @@ internal class DefaultTimeline(


prevDisplayIndex = initialDisplayIndex prevDisplayIndex = initialDisplayIndex
nextDisplayIndex = initialDisplayIndex nextDisplayIndex = initialDisplayIndex
if (initialEventId != null && shouldFetchInitialEvent) { val currentInitialEventId = initialEventId
fetchEvent(initialEventId) if (currentInitialEventId != null && shouldFetchInitialEvent) {
fetchEvent(currentInitialEventId)
} else { } else {
val count = Math.min(settings.initialSize, liveEvents.size) val count = Math.min(settings.initialSize, liveEvents.size)
if (isLive) { if (isLive) {
@ -543,10 +557,11 @@ internal class DefaultTimeline(
} }


private fun findCurrentChunk(realm: Realm): ChunkEntity? { private fun findCurrentChunk(realm: Realm): ChunkEntity? {
return if (initialEventId == null) { val currentInitialEventId = initialEventId
return if (currentInitialEventId == null) {
ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
} else { } else {
ChunkEntity.findIncludingEvent(realm, initialEventId) ChunkEntity.findIncludingEvent(realm, currentInitialEventId)
} }
} }


@ -566,10 +581,22 @@ internal class DefaultTimeline(
} }


private fun postSnapshot() { private fun postSnapshot() {
BACKGROUND_HANDLER.post {
val snapshot = createSnapshot() val snapshot = createSnapshot()
val runnable = Runnable { listener?.onUpdated(snapshot) } val runnable = Runnable { listener?.onUpdated(snapshot) }
debouncer.debounce("post_snapshot", runnable, 50) debouncer.debounce("post_snapshot", runnable, 50)
} }
}

private fun clearAllValues() {
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
builtEvents.clear()
builtEventsIdMap.clear()
backwardsPaginationState.set(PaginationState())
forwardsPaginationState.set(PaginationState())
}



// Extension methods *************************************************************************** // Extension methods ***************************************************************************



View File

@ -59,7 +59,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
cryptoService, cryptoService,
timelineEventMapper, timelineEventMapper,
settings, settings,
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings) TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
TimelineHiddenReadMarker(roomId)
) )
} }



View File

@ -0,0 +1,96 @@
/*

* 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.timeline

import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm
import io.realm.RealmObjectChangeListener
import io.realm.RealmResults

/**
* This class is responsible for handling the read marker for hidden events.
* When an hidden event has read marker, we want to transfer it on the first older displayed event.
* It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription.
*/
internal class TimelineHiddenReadMarker constructor(private val roomId: String) {

interface Delegate {
fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean
fun onReadMarkerUpdated()
}

private var previousDisplayedEventId: String? = null
private var readMarkerEntity: ReadMarkerEntity? = null

private lateinit var liveEvents: RealmResults<TimelineEventEntity>
private lateinit var delegate: Delegate

private val readMarkerListener = RealmObjectChangeListener<ReadMarkerEntity> { readMarker, _ ->
var hasChange = false
previousDisplayedEventId?.also {
hasChange = delegate.rebuildEvent(it, false)
previousDisplayedEventId = null
}
val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarker.eventId).findFirst() == null
if (isEventHidden) {
val hiddenEvent = readMarker.timelineEvent?.firstOrNull()
?: return@RealmObjectChangeListener
val displayIndex = hiddenEvent.root?.displayIndex
if (displayIndex != null) {
// Then we are looking for the first displayable event after the hidden one
val firstDisplayedEvent = liveEvents.where()
.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex)
.findFirst()

// If we find one, we should rebuild this one with marker
if (firstDisplayedEvent != null) {
previousDisplayedEventId = firstDisplayedEvent.eventId
hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true)
}
}
}
if (hasChange) delegate.onReadMarkerUpdated()
}


/**
* Start the realm query subscription. Has to be called on an HandlerThread
*/
fun start(realm: Realm, liveEvents: RealmResults<TimelineEventEntity>, delegate: Delegate) {
this.liveEvents = liveEvents
this.delegate = delegate
// We are looking for read receipts set on hidden events.
// We only accept those with a timelineEvent (so coming from pagination/sync).
readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId)
.findFirstAsync()
.also { it.addChangeListener(readMarkerListener) }

}

/**
* Dispose the realm query subscription. Has to be called on an HandlerThread
*/
fun dispose() {
this.readMarkerEntity?.removeAllChangeListeners()
}

}

View File

@ -22,7 +22,7 @@ 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.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
@ -33,7 +33,7 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject




internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService, internal class CryptoSyncHandler @Inject constructor(private val cryptoManager: CryptoManager,
private val sasVerificationService: DefaultSasVerificationService) { private val sasVerificationService: DefaultSasVerificationService) {


fun handleToDevice(toDevice: ToDeviceSyncResponse, initialSyncProgressService: DefaultInitialSyncProgressService? = null) { fun handleToDevice(toDevice: ToDeviceSyncResponse, initialSyncProgressService: DefaultInitialSyncProgressService? = null) {
@ -47,13 +47,13 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : " + event.content) Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : " + event.content)
} else { } else {
sasVerificationService.onToDeviceEvent(event) sasVerificationService.onToDeviceEvent(event)
cryptoService.onToDeviceEvent(event) cryptoManager.onToDeviceEvent(event)
} }
} }
} }


fun onSyncCompleted(syncResponse: SyncResponse) { fun onSyncCompleted(syncResponse: SyncResponse) {
cryptoService.onSyncCompleted(syncResponse) cryptoManager.onSyncCompleted(syncResponse)
} }




@ -68,7 +68,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
if (event.getClearType() == EventType.ENCRYPTED) { if (event.getClearType() == EventType.ENCRYPTED) {
var result: MXEventDecryptionResult? = null var result: MXEventDecryptionResult? = null
try { try {
result = cryptoService.decryptEvent(event, timelineId ?: "") result = cryptoManager.decryptEvent(event, timelineId ?: "")
} catch (exception: MXCryptoError) { } catch (exception: MXCryptoError) {
event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType //setCryptoError(exception.cryptoError) event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType //setCryptoError(exception.cryptoError)
} }

View File

@ -0,0 +1,51 @@
/*
* 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.sync

import im.vector.matrix.android.api.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
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.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject

internal class RoomFullyReadHandler @Inject constructor() {

fun handle(realm: Realm, roomId: String, content: FullyReadContent?) {
if (content == null) {
return
}
Timber.v("Handle for roomId: $roomId eventId: ${content.eventId}")

RoomSummaryEntity.getOrCreate(realm, roomId).apply {
readMarkerId = content.eventId
}
val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply {
eventId = content.eventId
}

// Remove the old marker if any
readMarkerEntity.timelineEvent?.firstOrNull()?.readMarker = null
// Attach to timelineEvent if known
val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst()
timelineEventEntity?.readMarker = readMarkerEntity
}

}

View File

@ -23,8 +23,13 @@ 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.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership 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.api.session.room.model.tag.RoomTagContent
import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.api.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvent
import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.helper.updateSenderDataFor
import im.vector.matrix.android.internal.database.model.ChunkEntity 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.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
@ -37,7 +42,11 @@ import im.vector.matrix.android.internal.session.notification.DefaultPushRuleSer
import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater 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.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.sync.model.* import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData
import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.session.user.UserEntityFactory
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
@ -50,7 +59,8 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
private val readReceiptHandler: ReadReceiptHandler, private val readReceiptHandler: ReadReceiptHandler,
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomTagHandler: RoomTagHandler, private val roomTagHandler: RoomTagHandler,
private val cryptoService: DefaultCryptoService, private val roomFullyReadHandler: RoomFullyReadHandler,
private val cryptoManager: CryptoManager,
private val tokenStore: SyncTokenStore, private val tokenStore: SyncTokenStore,
private val pushRuleService: DefaultPushRuleService, private val pushRuleService: DefaultPushRuleService,
private val processForPushTask: ProcessEventForPushTask, private val processForPushTask: ProcessEventForPushTask,
@ -97,12 +107,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
handleJoinedRoom(realm, it.key, it.value, isInitialSync) handleJoinedRoom(realm, it.key, it.value, isInitialSync)
} }
is HandlingStrategy.INVITED -> is HandlingStrategy.INVITED ->
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.1f) { handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.4f) {
handleInvitedRoom(realm, it.key, it.value) handleInvitedRoom(realm, it.key, it.value)
} }


is HandlingStrategy.LEFT -> { is HandlingStrategy.LEFT -> {
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_left_rooms, 0.3f) { handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_left_rooms, 0.2f) {
handleLeftRoom(realm, it.key, it.value) handleLeftRoom(realm, it.key, it.value)
} }
} }
@ -125,7 +135,8 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData) handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
} }


val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)


if (roomEntity.membership == Membership.INVITE) { if (roomEntity.membership == Membership.INVITE) {
roomEntity.chunks.deleteAllFromRealm() roomEntity.chunks.deleteAllFromRealm()
@ -134,12 +145,13 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch


// State event // State event
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) { if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt() ?: Int.MIN_VALUE val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
?: Int.MIN_VALUE
val untimelinedStateIndex = minStateIndex + 1 val untimelinedStateIndex = minStateIndex + 1
roomSync.state.events.forEach { event -> roomSync.state.events.forEach { event ->
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex) roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
// Give info to crypto module // Give info to crypto module
cryptoService.onStateEvent(roomId, event) cryptoManager.onStateEvent(roomId, event)
UserEntityFactory.createOrNull(event)?.also { UserEntityFactory.createOrNull(event)?.also {
realm.insertOrUpdate(it) realm.insertOrUpdate(it)
} }
@ -165,7 +177,8 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
roomSync: roomSync:
InvitedRoomSync): RoomEntity { InvitedRoomSync): RoomEntity {
Timber.v("Handle invited sync for room $roomId") Timber.v("Handle invited sync for room $roomId")
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
roomEntity.membership = Membership.INVITE roomEntity.membership = Membership.INVITE
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events) val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
@ -178,7 +191,8 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
private fun handleLeftRoom(realm: Realm, private fun handleLeftRoom(realm: Realm,
roomId: String, roomId: String,
roomSync: RoomSync): RoomEntity { roomSync: RoomSync): RoomEntity {
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)


roomEntity.membership = Membership.LEAVE roomEntity.membership = Membership.LEAVE
roomEntity.chunks.deleteAllFromRealm() roomEntity.chunks.deleteAllFromRealm()
@ -210,7 +224,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
event.eventId?.also { eventIds.add(it) } event.eventId?.also { eventIds.add(it) }
chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset) chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)
// Give info to crypto module // Give info to crypto module
cryptoService.onLiveEvent(roomEntity.roomId, event) cryptoManager.onLiveEvent(roomEntity.roomId, event)
// Try to remove local echo // Try to remove local echo
event.unsignedData?.transactionId?.also { event.unsignedData?.transactionId?.also {
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it) val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
@ -243,11 +257,16 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
} }


private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
accountData.events for (event in accountData.events) {
.asSequence() val eventType = event.getClearType()
.filter { it.getClearType() == EventType.TAG } if (eventType == EventType.TAG) {
.map { it.content.toModel<RoomTagContent>() } val content = event.getClearContent().toModel<RoomTagContent>()
.forEach { roomTagHandler.handle(realm, roomId, it) } roomTagHandler.handle(realm, roomId, content)
} else if (eventType == EventType.FULLY_READ) {
val content = event.getClearContent().toModel<FullyReadContent>()
roomFullyReadHandler.handle(realm, roomId, content)
}
}
} }


} }

View File

@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomTagEntity import im.vector.matrix.android.internal.database.model.RoomTagEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm import io.realm.Realm
import java.util.*
import javax.inject.Inject import javax.inject.Inject


internal class RoomTagHandler @Inject constructor() { internal class RoomTagHandler @Inject constructor() {
@ -29,8 +30,16 @@ internal class RoomTagHandler @Inject constructor() {
if (content == null) { if (content == null) {
return return
} }
val tags = content.tags.entries.map { (tagName, params) -> val tags = ArrayList<RoomTagEntity>()
RoomTagEntity(tagName, params["order"] as? Double) for (tagName in content.tags.keys) {
val params = content.tags[tagName]
val order = params?.get("order")
val tag = if (order is Double) {
RoomTagEntity(tagName, order)
} else {
RoomTagEntity(tagName, null)
}
tags.add(tag)
} }
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: RoomSummaryEntity(roomId) ?: RoomSummaryEntity(roomId)

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.session.sync


import arrow.core.Try import arrow.core.Try
import im.vector.matrix.android.R import im.vector.matrix.android.R
import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
import im.vector.matrix.android.internal.session.reportSubtask import im.vector.matrix.android.internal.session.reportSubtask
import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.session.sync.model.SyncResponse
@ -30,7 +30,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
private val userAccountDataSyncHandler: UserAccountDataSyncHandler, private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
private val groupSyncHandler: GroupSyncHandler, private val groupSyncHandler: GroupSyncHandler,
private val cryptoSyncHandler: CryptoSyncHandler, private val cryptoSyncHandler: CryptoSyncHandler,
private val cryptoService: DefaultCryptoService, private val cryptoManager: CryptoManager,
private val initialSyncProgressService: DefaultInitialSyncProgressService) { private val initialSyncProgressService: DefaultInitialSyncProgressService) {


fun handleResponse(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean): Try<SyncResponse> { fun handleResponse(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean): Try<SyncResponse> {
@ -40,12 +40,12 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
val reporter = initialSyncProgressService.takeIf { isInitialSync } val reporter = initialSyncProgressService.takeIf { isInitialSync }


measureTimeMillis { measureTimeMillis {
if (!cryptoService.isStarted()) { if (!cryptoManager.isStarted()) {
Timber.v("Should start cryptoService") Timber.v("Should start cryptoManager")
cryptoService.start(isInitialSync) cryptoManager.start(isInitialSync)
} }
}.also { }.also {
Timber.v("Finish handling start cryptoService in $it ms") Timber.v("Finish handling start cryptoManager in $it ms")
} }
val measure = measureTimeMillis { val measure = measureTimeMillis {
// Handle the to device events before the room ones // Handle the to device events before the room ones

View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.internal.util
import android.content.Context import android.content.Context
import androidx.work.WorkManager import androidx.work.WorkManager
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import java.util.UUID import java.util.*


internal class CancelableWork(private val context: Context, internal class CancelableWork(private val context: Context,
private val workId: UUID) : Cancelable { private val workId: UUID) : Cancelable {

View File

@ -34,7 +34,7 @@ import java.security.*
import java.security.cert.CertificateException import java.security.cert.CertificateException
import java.security.spec.AlgorithmParameterSpec import java.security.spec.AlgorithmParameterSpec
import java.security.spec.RSAKeyGenParameterSpec import java.security.spec.RSAKeyGenParameterSpec
import java.util.Calendar import java.util.*
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
import javax.crypto.* import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.GCMParameterSpec

View File

@ -22,7 +22,7 @@ import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import timber.log.Timber import timber.log.Timber
import java.util.TreeSet import java.util.*


/** /**
* Build canonical Json * Build canonical Json
@ -60,33 +60,43 @@ object JsonCanonicalizer {
when (any) { when (any) {
is JSONArray -> { is JSONArray -> {
// Canonicalize each element of the array // Canonicalize each element of the array
return (0 until any.length()).joinToString(separator = ",", prefix = "[", postfix = "]") { val result = StringBuilder("[")
canonicalizeRecursive(any.get(it))
for (i in 0 until any.length()) {
result.append(canonicalizeRecursive(any.get(i)))
if (i < any.length() - 1) {
result.append(",")
} }
} }

result.append("]")

return result.toString()
}
is JSONObject -> { is JSONObject -> {
// Sort the attributes by name, and the canonicalize each element of the JSONObject // Sort the attributes by name, and the canonicalize each element of the JSONObject
val result = StringBuilder("{")


val attributes = TreeSet<String>() val attributes = TreeSet<String>()
for (entry in any.keys()) { for (entry in any.keys()) {
attributes.add(entry) attributes.add(entry)
} }


return buildString { for (attribute in attributes.withIndex()) {
append("{") result.append("\"")
for ((index, value) in attributes.withIndex()) { .append(attribute.value)
append("\"") .append("\"")
append(value) .append(":")
append("\"") .append(canonicalizeRecursive(any[attribute.value]))
append(":")
append(canonicalizeRecursive(any[value]))


if (index < attributes.size - 1) { if (attribute.index < attributes.size - 1) {
append(",") result.append(",")
} }
} }
append("}")
} result.append("}")

return result.toString()
} }
is String -> return JSONObject.quote(any) is String -> return JSONObject.quote(any)
else -> return any.toString() else -> return any.toString()

View File

@ -167,9 +167,4 @@
<string name="initial_sync_start_importing_account_data">Начална синхронизация: <string name="initial_sync_start_importing_account_data">Начална синхронизация:
\nИмпортиране на данни за профила</string> \nИмпортиране на данни за профила</string>


<string name="notice_room_update">%s обнови тази стая.</string>

<string name="event_status_sending_message">Изпращане на съобщение…</string>
<string name="clear_timeline_send_queue">Изчисти опашката за изпращане</string>

</resources> </resources>

View File

@ -105,65 +105,4 @@
<string name="verification_emoji_pig">Schwein</string> <string name="verification_emoji_pig">Schwein</string>
<string name="verification_emoji_elephant">Elefant</string> <string name="verification_emoji_elephant">Elefant</string>
<string name="verification_emoji_rabbit">Hase</string> <string name="verification_emoji_rabbit">Hase</string>
<string name="notice_room_update">%s hat diesen Raum aufgewertet.</string>

<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Hahn</string>
<string name="verification_emoji_penguin">Pinguin</string>
<string name="verification_emoji_turtle">Schildkröte</string>
<string name="verification_emoji_fish">Fisch</string>
<string name="verification_emoji_octopus">Tintenfisch</string>
<string name="verification_emoji_butterfly">Schmetterling</string>
<string name="verification_emoji_flower">Blume</string>
<string name="verification_emoji_tree">Baum</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Pilz</string>
<string name="verification_emoji_globe">Globus</string>
<string name="verification_emoji_moon">Mond</string>
<string name="verification_emoji_cloud">Wolke</string>
<string name="verification_emoji_fire">Feuer</string>
<string name="verification_emoji_banana">Banane</string>
<string name="verification_emoji_apple">Apfel</string>
<string name="verification_emoji_strawberry">Erdbeere</string>
<string name="verification_emoji_corn">Mais</string>
<string name="verification_emoji_cake">Kuchen</string>
<string name="verification_emoji_heart">Herz</string>
<string name="verification_emoji_smiley">Lächeln</string>
<string name="verification_emoji_robot">Roboter</string>
<string name="verification_emoji_hat">Hut</string>
<string name="verification_emoji_glasses">Brille</string>
<string name="verification_emoji_wrench">Schraubenschlüssel</string>
<string name="verification_emoji_santa">Nikolaus</string>
<string name="verification_emoji_thumbsup">Daumen hoch</string>
<string name="verification_emoji_umbrella">Regenschirm</string>
<string name="verification_emoji_hourglass">Sanduhr</string>
<string name="verification_emoji_clock">Uhr</string>
<string name="verification_emoji_gift">Geschenk</string>
<string name="verification_emoji_lightbulb">Glühbirne</string>
<string name="verification_emoji_book">Buch</string>
<string name="verification_emoji_pencil">Stift</string>
<string name="verification_emoji_paperclip">Büroklammer</string>
<string name="verification_emoji_scissors">Scheren</string>
<string name="verification_emoji_lock">sperren</string>
<string name="verification_emoji_key">Schlüssel</string>
<string name="verification_emoji_hammer">Hammer</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Flagge</string>
<string name="verification_emoji_train">Zug</string>
<string name="verification_emoji_bicycle">Fahrrad</string>
<string name="verification_emoji_airplane">Flugzeug</string>
<string name="verification_emoji_rocket">Rakete</string>
<string name="verification_emoji_trophy">Pokal</string>
<string name="verification_emoji_ball">Ball</string>
<string name="verification_emoji_guitar">Gitarre</string>
<string name="verification_emoji_trumpet">Trompete</string>
<string name="verification_emoji_bell">Glocke</string>
<string name="verification_emoji_anchor">Anker</string>
<string name="verification_emoji_headphone">Kopfhörer</string>
<string name="verification_emoji_folder">Ordner</string>
<string name="verification_emoji_pin">Stecknadel</string>

<string name="event_status_sending_message">Sende eine Nachricht…</string>
<string name="clear_timeline_send_queue">Sendewarteschlange leeren</string>

</resources> </resources>

View File

@ -167,9 +167,4 @@
<string name="initial_sync_start_importing_account_data">Hasierako sinkronizazioa: <string name="initial_sync_start_importing_account_data">Hasierako sinkronizazioa:
\nKontuaren datuak inportatzen</string> \nKontuaren datuak inportatzen</string>


<string name="notice_room_update">%s erabiltzaileak gela hau eguneratu du.</string>

<string name="event_status_sending_message">Mezua bidaltzen…</string>
<string name="clear_timeline_send_queue">Garbitu bidalketa-ilara</string>

</resources> </resources>

View File

@ -168,9 +168,4 @@
<string name="initial_sync_start_importing_account_data">Alkusynkronointi: <string name="initial_sync_start_importing_account_data">Alkusynkronointi:
\nTuodaan tilin tietoja</string> \nTuodaan tilin tietoja</string>


<string name="notice_room_update">%s päivitti tämän huoneen.</string>

<string name="event_status_sending_message">Lähetetään viestiä…</string>
<string name="clear_timeline_send_queue">Tyhjennä lähetysjono</string>

</resources> </resources>

View File

@ -167,9 +167,4 @@
<string name="initial_sync_start_importing_account_data">Synchronisation initiale : <string name="initial_sync_start_importing_account_data">Synchronisation initiale :
\nImportation des données du compte</string> \nImportation des données du compte</string>


<string name="notice_room_update">%s a mis à niveau ce salon.</string>

<string name="event_status_sending_message">Envoi du message…</string>
<string name="clear_timeline_send_queue">Vider la file denvoi</string>

</resources> </resources>

View File

@ -166,9 +166,4 @@
<string name="initial_sync_start_importing_account_data">Induló szinkronizáció: <string name="initial_sync_start_importing_account_data">Induló szinkronizáció:
\nFiók adatok betöltése</string> \nFiók adatok betöltése</string>


<string name="notice_room_update">%s frissítette ezt a szobát.</string>

<string name="event_status_sending_message">Üzenet küldése…</string>
<string name="clear_timeline_send_queue">Küldő sor ürítése</string>

</resources> </resources>

View File

@ -167,9 +167,4 @@
<string name="initial_sync_start_importing_account_data">Sync iniziale: <string name="initial_sync_start_importing_account_data">Sync iniziale:
\nImportazione dati account</string> \nImportazione dati account</string>


<string name="notice_room_update">%s ha aggiornato questa stanza.</string>

<string name="event_status_sending_message">Invio messaggio in corso …</string>
<string name="clear_timeline_send_queue">Cancella la coda di invio</string>

</resources> </resources>

View File

@ -1,173 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<resources> <resources>
<string name="summary_message">%1$s: %2$s</string> <string name="summary_message">%1$s: %2$s</string>
<string name="notice_room_invite_no_invitee">%s님의 초대</string> <string name="notice_room_invite_no_invitee">%s\'의 초대</string>
<string name="verification_emoji_headphone">헤드폰</string> <string name="verification_emoji_headphone">헤드폰</string>
<string name="summary_user_sent_image">%1$s님이 사진을 보냈습니다.</string>
<string name="summary_user_sent_sticker">%1$s님이 스티커를 보냈습니다.</string>

<string name="notice_room_invite">%1$s님이 %2$s님을 초대했습니다</string>
<string name="notice_room_invite_you">%1$s님이 당신을 초대했습니다</string>
<string name="notice_room_join">%1$s님이 참가했습니다</string>
<string name="notice_room_leave">%1$s님이 떠났습니다</string>
<string name="notice_room_reject">%1$s님이 초대를 거부했습니다</string>
<string name="notice_room_kick">%1$s님이 %2$s님을 추방했습니다</string>
<string name="notice_room_unban">%1$s님이 %2$s님의 차단을 풀었습니다</string>
<string name="notice_room_ban">%1$s님이 %2$s님을 차단했습니다</string>
<string name="notice_room_withdraw">%1$s님이 %2$s님의 초대를 취소했습니다</string>
<string name="notice_avatar_url_changed">%1$s님이 아바타를 변경했습니다</string>
<string name="notice_display_name_set">%1$s님이 표시 이름을 %2$s(으)로 설정했습니다</string>
<string name="notice_display_name_changed_from">%1$s님이 표시 이름을 %2$s에서 %3$s(으)로 변경했습니다</string>
<string name="notice_display_name_removed">%1$s님이 표시 이름을 삭제했습니다 (%2$s)</string>
<string name="notice_room_topic_changed">%1$s님이 주제를 다음으로 변경했습니다: %2$s</string>
<string name="notice_room_name_changed">%1$s님이 방 이름을 다음으로 변경했습니다: %2$s</string>
<string name="notice_placed_video_call">%s님이 영상 통화를 걸었습니다.</string>
<string name="notice_placed_voice_call">%s님이 음성 통화를 걸었습니다.</string>
<string name="notice_answered_call">%s님이 전화를 받았습니다.</string>
<string name="notice_ended_call">%s님이 전화를 끊었습니다.</string>
<string name="notice_made_future_room_visibility">%1$s님이 이후 %2$s에게 방 기록을 공개했습니다</string>
<string name="notice_room_visibility_invited">초대된 시점부터 모든 방 구성원.</string>
<string name="notice_room_visibility_joined">들어온 시점부터 모든 방 구성원.</string>
<string name="notice_room_visibility_shared">모든 방 구성원.</string>
<string name="notice_room_visibility_world_readable">누구나.</string>
<string name="notice_room_visibility_unknown">알 수 없음 (%s).</string>
<string name="notice_end_to_end">%1$s님이 종단 간 암호화를 켰습니다 (%2$s)</string>
<string name="notice_room_update">%s님이 방을 업그레이드했습니다.</string>

<string name="notice_requested_voip_conference">%1$s님이 VoIP 회의를 요청했습니다</string>
<string name="notice_voip_started">VoIP 회의가 시작했습니다</string>
<string name="notice_voip_finished">VoIP 회의가 끝났습니다</string>

<string name="notice_avatar_changed_too">(아바타도 변경됨)</string>
<string name="notice_room_name_removed">%1$s님이 방 이름을 삭제했습니다</string>
<string name="notice_room_topic_removed">%1$s님이 방 주제를 삭제했습니다</string>
<string name="notice_event_redacted">메시지가 삭제되었습니다</string>
<string name="notice_event_redacted_by">메시지가 %1$s님에 의해 삭제되었습니다</string>
<string name="notice_event_redacted_with_reason">메시지가 삭제되었습니다 [이유: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">메시지가 %1$s님에 의해 삭제되었습니다 [이유: %2$s]</string>
<string name="notice_profile_change_redacted">%1$s님이 프로필 %2$s을(를) 업데이트했습니다</string>
<string name="notice_room_third_party_invite">%1$s님이 %2$s님에게 방 초대를 보냈습니다</string>
<string name="notice_room_third_party_registered_invite">%1$s님이 %2$s의 초대를 수락했습니다</string>

<string name="notice_crypto_unable_to_decrypt">** 암호를 해독할 수 없음: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string>

<string name="message_reply_to_prefix">이 답장의 질문</string>

<string name="could_not_redact">검열할 수 없습니다</string>
<string name="unable_to_send_message">메시지를 보낼 수 없습니다</string>

<string name="message_failed_to_upload">사진 업로드에 실패했습니다</string>

<string name="network_error">네트워크 오류</string>
<string name="matrix_error">Matrix 오류</string>

<string name="room_error_join_failed_empty_room">현재 빈 방에 다시 들어갈 수 없습니다.</string>

<string name="encrypted_message">암호화된 메시지</string>

<string name="medium_email">이메일 주소</string>
<string name="medium_phone_number">전화번호</string>

<string name="reply_to_an_image">사진을 보냈습니다.</string>
<string name="reply_to_a_video">동영상을 보냈습니다.</string>
<string name="reply_to_an_audio_file">오디오 파일을 보냈습니다.</string>
<string name="reply_to_a_file">파일을 보냈습니다.</string>

<string name="room_displayname_invite_from">%s에서 초대함</string>
<string name="room_displayname_room_invite">방 초대</string>

<string name="room_displayname_two_members">%1$s님과 %2$s님</string>

<plurals name="room_displayname_three_and_more_members">
<item quantity="other">%1$s님 외 %2$d명</item>
</plurals>

<string name="room_displayname_empty_room">빈 방</string>


<string name="verification_emoji_dog">개</string>
<string name="verification_emoji_cat">고양이</string>
<string name="verification_emoji_lion">사자</string>
<string name="verification_emoji_horse">말</string>
<string name="verification_emoji_unicorn">유니콘</string>
<string name="verification_emoji_pig">돼지</string>
<string name="verification_emoji_elephant">코끼리</string>
<string name="verification_emoji_rabbit">토끼</string>
<string name="verification_emoji_panda">판다</string>
<string name="verification_emoji_rooster">수탉</string>
<string name="verification_emoji_penguin">펭귄</string>
<string name="verification_emoji_turtle">거북</string>
<string name="verification_emoji_fish">물고기</string>
<string name="verification_emoji_octopus">문어</string>
<string name="verification_emoji_butterfly">나비</string>
<string name="verification_emoji_flower">꽃</string>
<string name="verification_emoji_tree">나무</string>
<string name="verification_emoji_cactus">선인장</string>
<string name="verification_emoji_mushroom">버섯</string>
<string name="verification_emoji_globe">지구본</string>
<string name="verification_emoji_moon">달</string>
<string name="verification_emoji_cloud">구름</string>
<string name="verification_emoji_fire">불</string>
<string name="verification_emoji_banana">바나나</string>
<string name="verification_emoji_apple">사과</string>
<string name="verification_emoji_strawberry">딸기</string>
<string name="verification_emoji_corn">옥수수</string>
<string name="verification_emoji_pizza">피자</string>
<string name="verification_emoji_cake">케이크</string>
<string name="verification_emoji_heart">하트</string>
<string name="verification_emoji_smiley">웃음</string>
<string name="verification_emoji_robot">로봇</string>
<string name="verification_emoji_hat">모자</string>
<string name="verification_emoji_glasses">안경</string>
<string name="verification_emoji_wrench">스패너</string>
<string name="verification_emoji_santa">산타클로스</string>
<string name="verification_emoji_thumbsup">좋아요</string>
<string name="verification_emoji_umbrella">우산</string>
<string name="verification_emoji_hourglass">모래시계</string>
<string name="verification_emoji_clock">시계</string>
<string name="verification_emoji_gift">선물</string>
<string name="verification_emoji_lightbulb">전구</string>
<string name="verification_emoji_book">책</string>
<string name="verification_emoji_pencil">연필</string>
<string name="verification_emoji_paperclip">클립</string>
<string name="verification_emoji_scissors">가위</string>
<string name="verification_emoji_lock">자물쇠</string>
<string name="verification_emoji_key">열쇠</string>
<string name="verification_emoji_hammer">망치</string>
<string name="verification_emoji_telephone">전화기</string>
<string name="verification_emoji_flag">깃발</string>
<string name="verification_emoji_train">기차</string>
<string name="verification_emoji_bicycle">자전거</string>
<string name="verification_emoji_airplane">비행기</string>
<string name="verification_emoji_rocket">로켓</string>
<string name="verification_emoji_trophy">트로피</string>
<string name="verification_emoji_ball">공</string>
<string name="verification_emoji_guitar">기타</string>
<string name="verification_emoji_trumpet">트럼펫</string>
<string name="verification_emoji_bell">종</string>
<string name="verification_emoji_anchor">닻</string>
<string name="verification_emoji_folder">폴더</string>
<string name="verification_emoji_pin">핀</string>

<string name="initial_sync_start_importing_account">초기 동기화:
\n계정 가져오는 중…</string>
<string name="initial_sync_start_importing_account_crypto">초기 동기화:
\n암호 가져오는 중</string>
<string name="initial_sync_start_importing_account_rooms">초기 동기화:
\n방 가져오는 중</string>
<string name="initial_sync_start_importing_account_joined_rooms">초기 동기화:
\n들어간 방 가져오는 중</string>
<string name="initial_sync_start_importing_account_invited_rooms">초기 동기화:
\n초대받은 방 가져오는 중</string>
<string name="initial_sync_start_importing_account_left_rooms">초기 동기화:
\n떠난 방 가져오는 중</string>
<string name="initial_sync_start_importing_account_groups">초기 동기화:
\n커뮤니티 가져오는 중</string>
<string name="initial_sync_start_importing_account_data">초기 동기화:
\n계정 데이터 가져오는 중</string>

<string name="event_status_sending_message">메시지 보내는 중…</string>
<string name="clear_timeline_send_queue">전송 대기 열 지우기</string>

</resources> </resources>

View File

@ -176,9 +176,4 @@
<string name="initial_sync_start_importing_account_data">Initiële synchronisatie: <string name="initial_sync_start_importing_account_data">Initiële synchronisatie:
\nAccountgegevens worden geïmporteerd</string> \nAccountgegevens worden geïmporteerd</string>


<string name="notice_room_update">%s heeft dit gesprek opgewaardeerd.</string>

<string name="event_status_sending_message">Bericht wordt verstuurd…</string>
<string name="clear_timeline_send_queue">Uitgaande wachtrij legen</string>

</resources> </resources>

View File

@ -7,7 +7,7 @@
<string name="notice_room_invite">%1$s zaprosił(a) %2$s</string> <string name="notice_room_invite">%1$s zaprosił(a) %2$s</string>
<string name="notice_room_invite_you">%1$s zaprosił(a) Cię</string> <string name="notice_room_invite_you">%1$s zaprosił(a) Cię</string>
<string name="notice_room_join">%1$s dołączył(a)</string> <string name="notice_room_join">%1$s dołączył(a)</string>
<string name="notice_room_leave">%1$s opuścił(a)</string> <string name="notice_room_leave">%1$s wyszedł(-ła)</string>
<string name="notice_room_reject">%1$s odrzucił(a) zaproszenie</string> <string name="notice_room_reject">%1$s odrzucił(a) zaproszenie</string>
<string name="notice_room_kick">%1$s wyrzucił(a) %2$s</string> <string name="notice_room_kick">%1$s wyrzucił(a) %2$s</string>
<string name="notice_room_unban">%1$s odblokował(a) %2$s</string> <string name="notice_room_unban">%1$s odblokował(a) %2$s</string>
@ -17,11 +17,11 @@
<string name="notice_display_name_changed_from">%1$s zmienił(a) wyświetlaną nazwę z %2$s na %3$s</string> <string name="notice_display_name_changed_from">%1$s zmienił(a) wyświetlaną nazwę z %2$s na %3$s</string>
<string name="notice_display_name_removed">%1$s usunął(-ęła) swoją wyświetlaną nazwę (%2$s)</string> <string name="notice_display_name_removed">%1$s usunął(-ęła) swoją wyświetlaną nazwę (%2$s)</string>
<string name="notice_room_topic_changed">%1$s zmienił(a) temat na: %2$s</string> <string name="notice_room_topic_changed">%1$s zmienił(a) temat na: %2$s</string>
<string name="unable_to_send_message">Nie można wysłać wiadomości</string> <string name="unable_to_send_message">Nie udało się wysłać wiadomości</string>


<string name="message_failed_to_upload">Przesyłanie zdjęcia nie powiodło się</string> <string name="message_failed_to_upload">Nie udało się wysłać zdjęcia</string>


<string name="network_error">Błąd sieci</string> <string name="network_error">ogólne błędy</string>
<string name="matrix_error">Błąd Matrixa</string> <string name="matrix_error">Błąd Matrixa</string>


<string name="encrypted_message">Wiadomość zaszyfrowana</string> <string name="encrypted_message">Wiadomość zaszyfrowana</string>
@ -31,7 +31,7 @@


<string name="notice_room_visibility_shared">wszyscy członkowie pokoju.</string> <string name="notice_room_visibility_shared">wszyscy członkowie pokoju.</string>
<string name="notice_room_visibility_world_readable">wszyscy.</string> <string name="notice_room_visibility_world_readable">wszyscy.</string>
<string name="notice_room_name_changed">%1$s zmienił(a) nazwę pokoju na: %2$s</string> <string name="notice_room_name_changed">%1$s zmienił(a) znawę pokoju na: %2$s</string>
<string name="notice_ended_call">%s zakończył(a) rozmowę.</string> <string name="notice_ended_call">%s zakończył(a) rozmowę.</string>
<string name="notice_room_name_removed">%1$s usunął(-ęła) nazwę pokoju</string> <string name="notice_room_name_removed">%1$s usunął(-ęła) nazwę pokoju</string>
<string name="notice_room_topic_removed">%1$s usunął(-ęła) temat pokoju</string> <string name="notice_room_topic_removed">%1$s usunął(-ęła) temat pokoju</string>
@ -57,9 +57,9 @@
</plurals> </plurals>


<string name="notice_crypto_unable_to_decrypt">** Nie można odszyfrować: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Nie można odszyfrować: %s **</string>
<string name="notice_placed_video_call">%s wykonał(a) rozmowę wideo.</string> <string name="notice_placed_video_call">%s umieścił wideo rozmowe.</string>
<string name="notice_placed_voice_call">%s wykonał(a) połączenie głosowe.</string> <string name="notice_placed_voice_call">%s umieścił połączenie głosowe.</string>
<string name="notice_made_future_room_visibility">%1$s uczynił(a) przyszłą historię pokoju widoczną dla %2$s</string> <string name="notice_made_future_room_visibility">%1$s uczynił historię pokoju widoczną do %2$s</string>
<string name="notice_room_visibility_invited">wszyscy członkowie pokoju, od momentu w którym zostali zaproszeni.</string> <string name="notice_room_visibility_invited">wszyscy członkowie pokoju, od momentu w którym zostali zaproszeni.</string>
<string name="notice_room_visibility_joined">wszyscy członkowie pokoju, od momentu w którym dołączyli.</string> <string name="notice_room_visibility_joined">wszyscy członkowie pokoju, od momentu w którym dołączyli.</string>
<string name="notice_room_visibility_unknown">nieznane (%s).</string> <string name="notice_room_visibility_unknown">nieznane (%s).</string>
@ -147,29 +147,4 @@
<string name="verification_emoji_santa">Mikołaj</string> <string name="verification_emoji_santa">Mikołaj</string>
<string name="verification_emoji_gift">Prezent</string> <string name="verification_emoji_gift">Prezent</string>
<string name="verification_emoji_hammer">Młotek</string> <string name="verification_emoji_hammer">Młotek</string>
<string name="notice_room_update">%s zakutalizował(a) ten pokój.</string>

<string name="verification_emoji_thumbsup">Kciuk w górę</string>
<string name="verification_emoji_lock">Zamek</string>
<string name="verification_emoji_ball">Piłka</string>
<string name="initial_sync_start_importing_account">Synchronizacja początkowa:
\nImportowanie konta…</string>
<string name="initial_sync_start_importing_account_crypto">Synchronizacja początkowa:
\nImportowanie kryptografii</string>
<string name="initial_sync_start_importing_account_rooms">Synchronizacja początkowa:
\nImportowanie Pokoi</string>
<string name="initial_sync_start_importing_account_joined_rooms">Synchronizacja początkowa:
\nImportowanie dołączonych Pokoi</string>
<string name="initial_sync_start_importing_account_invited_rooms">Synchronizacja początkowa:
\nImportowanie zaproszonych Pokoi</string>
<string name="initial_sync_start_importing_account_left_rooms">Synchronizacja początkowa:
\nImportowanie opuszczonych Pokoi</string>
<string name="initial_sync_start_importing_account_groups">Synchronizacja początkowa:
\nImportowanie Społeczności</string>
<string name="initial_sync_start_importing_account_data">Synchronizacja początkowa:
\nImportowanie danych Konta</string>

<string name="event_status_sending_message">Wysyłanie wiadomości…</string>
<string name="clear_timeline_send_queue">Wyczyść kolejkę wysyłania</string>

</resources> </resources>

View File

@ -78,10 +78,4 @@
<string name="room_displayname_empty_room">Sala vazia</string> <string name="room_displayname_empty_room">Sala vazia</string>




<string name="summary_user_sent_sticker">%1$s enviou um sticker.</string>

<string name="notice_room_update">%s fez o upgrade da sala.</string>

<string name="notice_event_redacted">Mensagem removida</string>
<string name="notice_event_redacted_by">Mensagem removida por %1$s</string>
</resources> </resources>

View File

@ -169,9 +169,9 @@
\nИмпорт криптографии</string> \nИмпорт криптографии</string>
<string name="initial_sync_start_importing_account_rooms">Начальная синхронизация: <string name="initial_sync_start_importing_account_rooms">Начальная синхронизация:
\nИмпорт комнат</string> \nИмпорт комнат</string>
<string name="initial_sync_start_importing_account_joined_rooms">Синхронизация начата: <string name="initial_sync_start_importing_account_joined_rooms">Начальная синхронизация:
\nИмпорт присоединенных комнат</string> \nИмпорт присоединенных комнат</string>
<string name="initial_sync_start_importing_account_invited_rooms">Синхронизация начата: <string name="initial_sync_start_importing_account_invited_rooms">Начальная синхронизация:
\nИмпорт приглашенных комнат</string> \nИмпорт приглашенных комнат</string>
<string name="initial_sync_start_importing_account_left_rooms">Начальная синхронизация: <string name="initial_sync_start_importing_account_left_rooms">Начальная синхронизация:
\nИмпорт покинутых комнат</string> \nИмпорт покинутых комнат</string>
@ -180,9 +180,4 @@
<string name="initial_sync_start_importing_account_data">Начальная синхронизация: <string name="initial_sync_start_importing_account_data">Начальная синхронизация:
\nИмпорт данных учетной записи</string> \nИмпорт данных учетной записи</string>


<string name="notice_room_update">%s обновил эту комнату.</string>

<string name="event_status_sending_message">Отправка сообщения…</string>
<string name="clear_timeline_send_queue">Очистить очередь отправки</string>

</resources> </resources>

View File

@ -82,95 +82,4 @@
</plurals> </plurals>




<string name="notice_room_update">%s aktualizoval túto miestnosť.</string>

<string name="notice_event_redacted">Správa odstránená</string>
<string name="notice_event_redacted_by">Správa odstránená používateľom %1$s</string>
<string name="notice_event_redacted_with_reason">Správa odstránená [dôvod: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Správa odstránená používateľom %1$s [dôvod: %2$s]</string>
<string name="verification_emoji_dog">Pes</string>
<string name="verification_emoji_cat">Mačka</string>
<string name="verification_emoji_lion">Lev</string>
<string name="verification_emoji_horse">Kôň</string>
<string name="verification_emoji_unicorn">Jednorožec</string>
<string name="verification_emoji_pig">Prasa</string>
<string name="verification_emoji_elephant">Slon</string>
<string name="verification_emoji_rabbit">Zajac</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kohút</string>
<string name="verification_emoji_penguin">Tučniak</string>
<string name="verification_emoji_turtle">Korytnačka</string>
<string name="verification_emoji_fish">Ryba</string>
<string name="verification_emoji_octopus">Chobotnica</string>
<string name="verification_emoji_butterfly">Motýľ</string>
<string name="verification_emoji_flower">Kvetina</string>
<string name="verification_emoji_tree">Strom</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Hríb</string>
<string name="verification_emoji_globe">Zemeguľa</string>
<string name="verification_emoji_moon">Mesiac</string>
<string name="verification_emoji_cloud">Oblak</string>
<string name="verification_emoji_fire">Oheň</string>
<string name="verification_emoji_banana">Banán</string>
<string name="verification_emoji_apple">Jablko</string>
<string name="verification_emoji_strawberry">Jahoda</string>
<string name="verification_emoji_corn">Kukurica</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Koláč</string>
<string name="verification_emoji_heart">Srdce</string>
<string name="verification_emoji_smiley">Úsmev</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Klobúk</string>
<string name="verification_emoji_glasses">Okuliare</string>
<string name="verification_emoji_wrench">Skrutkovač</string>
<string name="verification_emoji_santa">Mikuláš</string>
<string name="verification_emoji_thumbsup">Palec nahor</string>
<string name="verification_emoji_umbrella">Dáždnik</string>
<string name="verification_emoji_hourglass">Presýpacie hodiny</string>
<string name="verification_emoji_clock">Hodiny</string>
<string name="verification_emoji_gift">Darček</string>
<string name="verification_emoji_lightbulb">Žiarovka</string>
<string name="verification_emoji_book">Kniha</string>
<string name="verification_emoji_pencil">Ceruzka</string>
<string name="verification_emoji_paperclip">Kancelárska sponka</string>
<string name="verification_emoji_scissors">Nožnice</string>
<string name="verification_emoji_lock">Zámok</string>
<string name="verification_emoji_key">Kľúč</string>
<string name="verification_emoji_hammer">Kladivo</string>
<string name="verification_emoji_telephone">Telefón</string>
<string name="verification_emoji_flag">Vlajka</string>
<string name="verification_emoji_train">Vlak</string>
<string name="verification_emoji_bicycle">Bicykel</string>
<string name="verification_emoji_airplane">Lietadlo</string>
<string name="verification_emoji_rocket">Raketa</string>
<string name="verification_emoji_trophy">Trofej</string>
<string name="verification_emoji_ball">Lopta</string>
<string name="verification_emoji_guitar">Gitara</string>
<string name="verification_emoji_trumpet">Trúbka</string>
<string name="verification_emoji_bell">Zvonček</string>
<string name="verification_emoji_anchor">Kotva</string>
<string name="verification_emoji_headphone">Schlúchadlá</string>
<string name="verification_emoji_folder">Priečinok</string>
<string name="verification_emoji_pin">Pin</string>

<string name="initial_sync_start_importing_account">Úvodná synchronizácia:
\nPrebieha import účtu…</string>
<string name="initial_sync_start_importing_account_crypto">Úvodná synchronizácia:
\nPrebieha import šifrovacích kľúčov</string>
<string name="initial_sync_start_importing_account_rooms">Úvodná synchronizácia:
\nPrebieha import miestností</string>
<string name="initial_sync_start_importing_account_joined_rooms">Úvodná synchronizácia:
\nPrebieha import miestností, do ktorých ste vstúpili</string>
<string name="initial_sync_start_importing_account_invited_rooms">Úvodná synchronizácia:
\nPrebieha import pozvánok</string>
<string name="initial_sync_start_importing_account_left_rooms">Úvodná synchronizácia:
\nPrebieha import opustených miestností</string>
<string name="initial_sync_start_importing_account_groups">Úvodná synchronizácia:
\nPrebieha import komunít</string>
<string name="initial_sync_start_importing_account_data">Úvodná synchronizácia:
\nPrebieha import údajov účtu</string>

<string name="event_status_sending_message">Odosielanie správy…</string>
<string name="clear_timeline_send_queue">Vymazať správy na odoslanie</string>

</resources> </resources>

View File

@ -146,26 +146,4 @@
<string name="verification_emoji_anchor">Spirancë</string> <string name="verification_emoji_anchor">Spirancë</string>
<string name="verification_emoji_headphone">Kufje</string> <string name="verification_emoji_headphone">Kufje</string>
<string name="verification_emoji_folder">Dosje</string> <string name="verification_emoji_folder">Dosje</string>
<string name="notice_room_update">%s e përmirësoi këtë dhomë.</string>

<string name="initial_sync_start_importing_account">Njëkohësimi Fillestar:
\nPo importohet llogaria…</string>
<string name="initial_sync_start_importing_account_crypto">Njëkohësimi Fillestar:
\nPo importohet kriptografi</string>
<string name="initial_sync_start_importing_account_rooms">Njëkohësimi Fillestar:
\nPo importohen Dhoma</string>
<string name="initial_sync_start_importing_account_joined_rooms">Njëkohësimi Fillestar:
\nPo importohen Dhoma Ku Është Bërë Hyrje</string>
<string name="initial_sync_start_importing_account_invited_rooms">Njëkohësimi Fillestar:
\nPo importohen Dhoma Me Ftesë</string>
<string name="initial_sync_start_importing_account_left_rooms">Njëkohësimi Fillestar:
\nPo importohen Dhoma të Braktisura</string>
<string name="initial_sync_start_importing_account_groups">Njëkohësimi Fillestar:
\nPo importohen Bashkësi</string>
<string name="initial_sync_start_importing_account_data">Njëkohësimi Fillestar:
\nPo importohet të Dhëna Llogarie</string>

<string name="event_status_sending_message">Po dërgohet mesazh…</string>
<string name="clear_timeline_send_queue">Spastro radhë pritjeje</string>

</resources> </resources>

View File

@ -48,7 +48,7 @@
<string name="notice_room_third_party_registered_invite">%1$s èt duutnodigienge vo %2$s anveird</string> <string name="notice_room_third_party_registered_invite">%1$s èt duutnodigienge vo %2$s anveird</string>


<string name="notice_crypto_unable_to_decrypt">** Kun nie ountsleuteln: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Kun nie ountsleuteln: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">t Toestel van den afzender èt geen sleutels vo da bericht hier gesteurd.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">t Toestel van den afzender èt geen sleutels vo dit bericht gesteurd.</string>


<string name="message_reply_to_prefix">Als antwoord ip</string> <string name="message_reply_to_prefix">Als antwoord ip</string>


@ -150,26 +150,21 @@
<string name="verification_emoji_folder">Mappe</string> <string name="verification_emoji_folder">Mappe</string>
<string name="verification_emoji_pin">Pinne</string> <string name="verification_emoji_pin">Pinne</string>


<string name="initial_sync_start_importing_account">Initiële synchronisoasje: <string name="initial_sync_start_importing_account">Initiële synchronisoatie:
\nAccount wor geïmporteerd…</string> \nAccount wor geïmporteerd…</string>
<string name="initial_sync_start_importing_account_crypto">Initiële synchronisoasje: <string name="initial_sync_start_importing_account_crypto">Initiële synchronisoatie:
\nCrypto wor geïmporteerd</string> \nCrypto wor geïmporteerd</string>
<string name="initial_sync_start_importing_account_rooms">Initiële synchronisoasje: <string name="initial_sync_start_importing_account_rooms">Initiële synchronisoatie:
\nGesprekkn wordn geïmporteerd</string> \nGesprekkn wordn geïmporteerd</string>
<string name="initial_sync_start_importing_account_joined_rooms">Initiële synchronisoasje: <string name="initial_sync_start_importing_account_joined_rooms">Initiële synchronisoatie:
\nDeelgenoomn gesprekken wordn geïmporteerd</string> \nDeelgenoomn gesprekken wordn geïmporteerd</string>
<string name="initial_sync_start_importing_account_invited_rooms">Initiële synchronisoasje: <string name="initial_sync_start_importing_account_invited_rooms">Initiële synchronisoatie:
\nUutgenodigde gesprekkn wordn geïmporteerd</string> \nUutgenodigde gesprekkn wordn geïmporteerd</string>
<string name="initial_sync_start_importing_account_left_rooms">Initiële synchronisoasje: <string name="initial_sync_start_importing_account_left_rooms">Initiële synchronisoatie:
\nVerloatn gesprekkn wordn geïmporteerd</string> \nVerloatn gesprekkn wordn geïmporteerd</string>
<string name="initial_sync_start_importing_account_groups">Initiële synchronisoasje: <string name="initial_sync_start_importing_account_groups">Initiële synchronisoatie:
\nGemeenschappn wordn geïmporteerd</string> \nGemeenschappn wordn geïmporteerd</string>
<string name="initial_sync_start_importing_account_data">Initiële synchronisoasje: <string name="initial_sync_start_importing_account_data">Initiële synchronisoatie:
\nAccountgegeevns wordn geïmporteerd</string> \nAccountgegeevns wordn geïmporteerd</string>


<string name="notice_room_update">%s èt da gesprek hier ipgewoardeerd.</string>

<string name="event_status_sending_message">Bericht wor verstuurd…</string>
<string name="clear_timeline_send_queue">Uutgoande wachtreeke leegn</string>

</resources> </resources>

View File

@ -162,9 +162,4 @@
<string name="initial_sync_start_importing_account_data">初始化同步: <string name="initial_sync_start_importing_account_data">初始化同步:
\n正在导入账号数据</string> \n正在导入账号数据</string>


<string name="notice_room_update">%s 升级了聊天室。</string>

<string name="event_status_sending_message">正在发送消息…</string>
<string name="clear_timeline_send_queue">清除正在发送队列</string>

</resources> </resources>

View File

@ -165,9 +165,4 @@
<string name="initial_sync_start_importing_account_data">初始化同步: <string name="initial_sync_start_importing_account_data">初始化同步:
\n正在匯入帳號資料</string> \n正在匯入帳號資料</string>


<string name="notice_room_update">%s 已升級此聊天室。</string>

<string name="event_status_sending_message">正在傳送訊息……</string>
<string name="clear_timeline_send_queue">清除傳送佇列</string>

</resources> </resources>

View File

@ -15,7 +15,7 @@ androidExtensions {
} }


ext.versionMajor = 0 ext.versionMajor = 0
ext.versionMinor = 5 ext.versionMinor = 3
ext.versionPatch = 0 ext.versionPatch = 0


static def getGitTimestamp() { static def getGitTimestamp() {
@ -51,7 +51,7 @@ static def gitRevisionDate() {
} }


static def gitBranchName() { static def gitBranchName() {
def cmd = "git rev-parse --abbrev-ref HEAD" def cmd = "git name-rev --name-only HEAD"
return cmd.execute().text.trim() return cmd.execute().text.trim()
} }


@ -318,8 +318,6 @@ dependencies {


implementation 'diff_match_patch:diff_match_patch:current' implementation 'diff_match_patch:diff_match_patch:current'


implementation "androidx.emoji:emoji-appcompat:1.0.0"

// TESTS // TESTS
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'

View File

@ -1,67 +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.riotx

import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import androidx.core.provider.FontRequest
import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.FontRequestEmojiCompatConfig
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class EmojiCompatWrapper @Inject constructor(private val context: Context) {

private var initialized = false

fun init(fontRequest: FontRequest) {

//Use emoji compat for the benefit of emoji spans
val config = FontRequestEmojiCompatConfig(context, fontRequest)
// we want to replace all emojis with selected font
.setReplaceAll(true)
//Debug options
// .setEmojiSpanIndicatorEnabled(true)
// .setEmojiSpanIndicatorColor(Color.GREEN)
EmojiCompat.init(config)
.registerInitCallback(object : EmojiCompat.InitCallback() {
override fun onInitialized() {
Timber.v("Emoji compat onInitialized success ")
initialized = true
}

override fun onFailed(throwable: Throwable?) {
Timber.e(throwable, "Failed to init EmojiCompat")
}
})
}

fun safeEmojiSpanify(sequence: CharSequence): CharSequence {
if (initialized) {
try {
return EmojiCompat.get().process(sequence)
} catch (throwable: Throwable) {
//Defensive coding against error (should not happend as it is initialized)
Timber.e(throwable, "Failed to init EmojiCompat")
return sequence
}
} else {
return sequence
}
}
}

View File

@ -66,7 +66,6 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var authenticator: Authenticator @Inject lateinit var authenticator: Authenticator
@Inject lateinit var vectorConfiguration: VectorConfiguration @Inject lateinit var vectorConfiguration: VectorConfiguration
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider @Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
@Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@ -86,12 +85,9 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
vectorComponent = DaggerVectorComponent.factory().create(this) vectorComponent = DaggerVectorComponent.factory().create(this)
vectorComponent.inject(this) vectorComponent.inject(this)
vectorUncaughtExceptionHandler.activate(this) vectorUncaughtExceptionHandler.activate(this)

// Log
if (BuildConfig.DEBUG) { VectorFileLogger.init(this)
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree(), VectorFileLogger)
}
Timber.plant(vectorComponent.vectorFileLogger())

if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Stetho.initializeWithDefaults(this) Stetho.initializeWithDefaults(this)
} }
@ -109,9 +105,6 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
) )
FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler()) FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler())
vectorConfiguration.initConfiguration() vectorConfiguration.initConfiguration()

emojiCompatWrapper.init(fontRequest)

NotificationUtils.createNotificationChannels(applicationContext) NotificationUtils.createNotificationChannels(applicationContext)
if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!! val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!!

View File

@ -58,7 +58,6 @@ import im.vector.riotx.features.rageshake.BugReportActivity
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.RageShake import im.vector.riotx.features.rageshake.RageShake
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
import im.vector.riotx.features.reactions.widget.ReactionButton
import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
@ -182,8 +181,6 @@ interface ScreenComponent {


fun inject(displayReadReceiptsBottomSheet: DisplayReadReceiptsBottomSheet) fun inject(displayReadReceiptsBottomSheet: DisplayReadReceiptsBottomSheet)


fun inject(reactionButton: ReactionButton)

@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(vectorComponent: VectorComponent, fun create(vectorComponent: VectorComponent,

View File

@ -24,7 +24,6 @@ import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.VectorApplication import im.vector.riotx.VectorApplication
import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.configuration.VectorConfiguration
@ -41,7 +40,6 @@ import im.vector.riotx.features.notifications.NotificationBroadcastReceiver
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import javax.inject.Singleton import javax.inject.Singleton
@ -72,8 +70,6 @@ interface VectorComponent {


fun emojiCompatFontProvider(): EmojiCompatFontProvider fun emojiCompatFontProvider(): EmojiCompatFontProvider


fun emojiCompatWrapper() : EmojiCompatWrapper

fun eventHtmlRenderer(): EventHtmlRenderer fun eventHtmlRenderer(): EventHtmlRenderer


fun navigator(): Navigator fun navigator(): Navigator
@ -102,8 +98,6 @@ interface VectorComponent {


fun vectorPreferences(): VectorPreferences fun vectorPreferences(): VectorPreferences


fun vectorFileLogger(): VectorFileLogger

@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(@BindsInstance context: Context): VectorComponent fun create(@BindsInstance context: Context): VectorComponent

View File

@ -0,0 +1,75 @@
/*

* 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.riotx.core.ui.views

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import butterknife.ButterKnife
import im.vector.riotx.R
import im.vector.riotx.features.themes.ThemeUtils
import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.*
import me.gujun.android.span.span
import me.saket.bettermovementmethod.BetterLinkMovementMethod

class JumpToReadMarkerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {

interface Callback {
fun onJumpToReadMarkerClicked(readMarkerId: String)
fun onClearReadMarkerClicked()
}

var callback: Callback? = null

init {
setupView()
}

private fun setupView() {
LinearLayout.inflate(context, R.layout.view_jump_to_read_marker, this)
setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color))
jumpToReadMarkerLabelView.movementMethod = BetterLinkMovementMethod.getInstance()
isClickable = true
closeJumpToReadMarkerView.setOnClickListener {
visibility = View.GONE
callback?.onClearReadMarkerClicked()
}
}

fun render(show: Boolean, readMarkerId: String?) {
isVisible = show
if (readMarkerId != null) {
jumpToReadMarkerLabelView.text = span(resources.getString(R.string.room_jump_to_first_unread)) {
textDecorationLine = "underline"
onClick = { callback?.onJumpToReadMarkerClicked(readMarkerId) }
}
}

}


}

View File

@ -0,0 +1,86 @@
/*

* 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.riotx.core.ui.views

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import im.vector.riotx.R
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.coroutines.*

private const val DELAY_IN_MS = 1_500L

class ReadMarkerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

interface Callback {
fun onReadMarkerDisplayed()
}

private var callback: Callback? = null
private var callbackDispatcherJob: Job? = null

fun bindView(informationData: MessageInformationData, readMarkerCallback: Callback) {
this.callback = readMarkerCallback
if (informationData.displayReadMarker) {
visibility = VISIBLE
callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) {
delay(DELAY_IN_MS)
callback?.onReadMarkerDisplayed()
}
startAnimation()
} else {
visibility = INVISIBLE
}

}

fun unbind() {
this.callbackDispatcherJob?.cancel()
this.callback = null
this.animation?.cancel()
this.visibility = INVISIBLE
}

private fun startAnimation() {
if (animation == null) {
animation = AnimationUtils.loadAnimation(context, R.anim.unread_marker_anim)
animation.startOffset = DELAY_IN_MS / 2
animation.duration = DELAY_IN_MS / 2
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
}

override fun onAnimationEnd(animation: Animation) {
visibility = INVISIBLE
}

override fun onAnimationRepeat(animation: Animation) {}
})
}
animation.start()
}

}

View File

@ -21,8 +21,11 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import butterknife.ButterKnife
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import kotlinx.android.synthetic.main.view_read_receipts.view.* import kotlinx.android.synthetic.main.view_read_receipts.view.*
@ -45,6 +48,7 @@ class ReadReceiptsView @JvmOverloads constructor(


private fun setupView() { private fun setupView() {
inflate(context, R.layout.view_read_receipts, this) inflate(context, R.layout.view_read_receipts, this)
ButterKnife.bind(this)
} }


fun render(readReceipts: List<ReadReceiptData>, avatarRenderer: AvatarRenderer, clickListener: OnClickListener) { fun render(readReceipts: List<ReadReceiptData>, avatarRenderer: AvatarRenderer, clickListener: OnClickListener) {

View File

@ -26,6 +26,7 @@ import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
@ -35,10 +36,12 @@ import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.disclaimer.showDisclaimerDialog
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.workers.signout.SignOutViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel
@ -116,22 +119,22 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
} }


activeSessionHolder.getSafeActiveSession()?.getInitialSyncProgressStatus()?.observe(this, Observer { status -> activeSessionHolder.getSafeActiveSession()?.getLiveStatus()?.observe(this, Observer { sprogress ->
if (status == null) { Timber.e("${sprogress?.statusText?.let { getString(it) }} ${sprogress?.percentProgress}")
if (sprogress == null) {
waiting_view.isVisible = false waiting_view.isVisible = false
} else { } else {
Timber.e("${getString(status.statusText)} ${status.percentProgress}")
waiting_view.setOnClickListener { waiting_view.setOnClickListener {
//block interactions //block interactions
} }
waiting_view_status_horizontal_progress.apply { waiting_view_status_horizontal_progress.apply {
isIndeterminate = false isIndeterminate = false
max = 100 max = 100
progress = status.percentProgress progress = sprogress.percentProgress
isVisible = true isVisible = true
} }
waiting_view_status_text.apply { waiting_view_status_text.apply {
text = getString(status.statusText) text = sprogress.statusText?.let { getString(it) }
isVisible = true isVisible = true
} }
waiting_view.isVisible = true waiting_view.isVisible = true
@ -210,6 +213,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
} }






companion object { companion object {
private const val EXTRA_CLEAR_EXISTING_NOTIFICATION = "EXTRA_CLEAR_EXISTING_NOTIFICATION" private const val EXTRA_CLEAR_EXISTING_NOTIFICATION = "EXTRA_CLEAR_EXISTING_NOTIFICATION"



View File

@ -0,0 +1,25 @@
/*
* 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.riotx.features.home.room.detail

import java.io.File

data class DownloadFileState(
val mimeType: String,
val file: File?,
val throwable: Throwable?
)

View File

@ -18,7 +18,6 @@ package im.vector.riotx.features.home.room.detail


import com.jaiselrahman.filepicker.model.MediaFile import com.jaiselrahman.filepicker.model.MediaFile
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
@ -27,13 +26,16 @@ sealed class RoomDetailActions {


data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions() data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions() data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions() data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions() data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions() data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions() data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions()
data class SetReadMarkerAction(val eventId: String) : RoomDetailActions()
object MarkAllAsRead : RoomDetailActions()
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions() data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions()
data class HandleTombstoneEvent(val event: Event) : RoomDetailActions() data class HandleTombstoneEvent(val event: Event) : RoomDetailActions()
object AcceptInvite : RoomDetailActions() object AcceptInvite : RoomDetailActions()
@ -47,5 +49,4 @@ sealed class RoomDetailActions {
object ClearSendQueue : RoomDetailActions() object ClearSendQueue : RoomDetailActions()
object ResendAll : RoomDetailActions() object ResendAll : RoomDetailActions()



} }

View File

@ -28,7 +28,12 @@ import android.os.Parcelable
import android.text.Editable import android.text.Editable
import android.text.Spannable import android.text.Spannable
import android.text.TextUtils import android.text.TextUtils
import android.view.* import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.Window
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
@ -46,7 +51,12 @@ import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.* import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -60,7 +70,13 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
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.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
@ -77,9 +93,21 @@ import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.* import im.vector.riotx.core.ui.views.JumpToReadMarkerView
import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard
import im.vector.riotx.core.utils.openCamera
import im.vector.riotx.core.utils.shareMedia
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
@ -94,9 +122,18 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.action.* import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction
import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.invite.VectorInviteView
@ -134,7 +171,8 @@ class RoomDetailFragment :
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
AutocompleteUserPresenter.Callback, AutocompleteUserPresenter.Callback,
VectorInviteView.Callback { VectorInviteView.Callback,
JumpToReadMarkerView.Callback {


companion object { companion object {


@ -194,6 +232,7 @@ class RoomDetailFragment :
override fun getMenuRes() = R.menu.menu_timeline override fun getMenuRes() = R.menu.menu_timeline


private lateinit var actionViewModel: ActionsHandler private lateinit var actionViewModel: ActionsHandler
private lateinit var layoutManager: LinearLayoutManager


@BindView(R.id.composerLayout) @BindView(R.id.composerLayout)
lateinit var composerLayout: TextComposerView lateinit var composerLayout: TextComposerView
@ -211,6 +250,7 @@ class RoomDetailFragment :
setupAttachmentButton() setupAttachmentButton()
setupInviteView() setupInviteView()
setupNotificationView() setupNotificationView()
setupJumpToReadMarkerView()
roomDetailViewModel.subscribe { renderState(it) } roomDetailViewModel.subscribe { renderState(it) }
textComposerViewModel.subscribe { renderTextComposerState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
@ -224,8 +264,12 @@ class RoomDetailFragment :
} }


roomDetailViewModel.navigateToEvent.observeEvent(this) { roomDetailViewModel.navigateToEvent.observeEvent(this) {
// val scrollPosition = timelineEventController.searchPositionOfEvent(it)
if (scrollPosition == null) {
scrollOnHighlightedEventCallback.scheduleScrollTo(it) scrollOnHighlightedEventCallback.scheduleScrollTo(it)
} else {
layoutManager.scrollToPosition(scrollPosition)
}
} }


roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) { roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
@ -259,6 +303,10 @@ class RoomDetailFragment :
} }
} }


private fun setupJumpToReadMarkerView() {
jumpToReadMarkerView.callback = this
}

private fun setupNotificationView() { private fun setupNotificationView() {
notificationAreaView.delegate = object : NotificationAreaView.Delegate { notificationAreaView.delegate = object : NotificationAreaView.Delegate {


@ -380,7 +428,7 @@ class RoomDetailFragment :
private fun setupRecyclerView() { private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker() val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView) epoxyVisibilityTracker.attach(recyclerView)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController) scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
@ -405,7 +453,7 @@ class RoomDetailFragment :
R.drawable.ic_reply, R.drawable.ic_reply,
object : RoomMessageTouchHelperCallback.QuickReplayHandler { object : RoomMessageTouchHelperCallback.QuickReplayHandler {
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
(model as? AbsMessageItem)?.informationData?.let { (model as? AbsMessageItem)?.attributes?.informationData?.let {
val eventId = it.eventId val eventId = it.eventId
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
} }
@ -416,7 +464,7 @@ class RoomDetailFragment :
is MessageFileItem, is MessageFileItem,
is MessageImageVideoItem, is MessageImageVideoItem,
is MessageTextItem -> { is MessageTextItem -> {
return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
} }
else -> false else -> false
} }
@ -585,7 +633,7 @@ class RoomDetailFragment :
val summary = state.asyncRoomSummary() val summary = state.asyncRoomSummary()
val inviter = state.asyncInviter() val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) { if (summary?.membership == Membership.JOIN) {
timelineEventController.setTimeline(state.timeline, state.eventId) timelineEventController.setTimeline(state.timeline, state.highlightedEventId)
inviteView.visibility = View.GONE inviteView.visibility = View.GONE
val uid = session.myUserId val uid = session.myUserId
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
@ -608,10 +656,12 @@ class RoomDetailFragment :
composerLayout.visibility = View.GONE composerLayout.visibility = View.GONE
notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent))
} }
jumpToReadMarkerView.render(state.showJumpToReadMarker, summary?.readMarkerId)
} }


private fun renderRoomSummary(state: RoomDetailViewState) { private fun renderRoomSummary(state: RoomDetailViewState) {
state.asyncRoomSummary()?.let { state.asyncRoomSummary()?.let {

if (it.membership.isLeft()) { if (it.membership.isLeft()) {
Timber.w("The room has been left") Timber.w("The room has been left")
activity?.finish() activity?.finish()
@ -696,7 +746,7 @@ class RoomDetailFragment :
showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room))
} else { } else {
// Highlight and scroll to this event // Highlight and scroll to this event
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, timelineEventController.searchPositionOfEvent(eventId))) roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, true))
} }
return true return true
} }
@ -716,7 +766,11 @@ class RoomDetailFragment :
} }


override fun onEventVisible(event: TimelineEvent) { override fun onEventVisible(event: TimelineEvent) {
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event)) roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsVisible(event))
}

override fun onEventInvisible(event: TimelineEvent) {
roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsInvisible(event))
} }


override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) { override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) {
@ -836,6 +890,14 @@ class RoomDetailFragment :
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
} }


override fun onReadMarkerLongDisplayed(informationData: MessageInformationData) {
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
val eventId = timelineEventController.searchEventIdAtPosition(firstVisibleItem)
if (eventId != null) {
roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(eventId))
}
}

// AutocompleteUserPresenter.Callback // AutocompleteUserPresenter.Callback


override fun onQueryUsers(query: CharSequence?) { override fun onQueryUsers(query: CharSequence?) {
@ -1012,4 +1074,16 @@ class RoomDetailFragment :
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
roomDetailViewModel.process(RoomDetailActions.RejectInvite) roomDetailViewModel.process(RoomDetailActions.RejectInvite)
} }

// JumpToReadMarkerView.Callback

override fun onJumpToReadMarkerClicked(readMarkerId: String) {
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false))
}

override fun onClearReadMarkerClicked() {
roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead)
}


} }

View File

@ -38,6 +38,7 @@ import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.message.getFileUrl
@ -58,6 +59,8 @@ import im.vector.riotx.features.command.CommandParser
import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.command.ParsedCommand
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
@ -75,7 +78,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)!!
private val roomId = initialState.roomId private val roomId = initialState.roomId
private val eventId = initialState.eventId private val eventId = initialState.eventId
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>() private val invisibleEventsObservable = BehaviorRelay.create<RoomDetailActions.TimelineEventTurnsInvisible>()
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailActions.TimelineEventTurnsVisible>()
private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) {
TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts()) TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts())
} else { } else {
@ -109,6 +113,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
observeRoomSummary() observeRoomSummary()
observeEventDisplayedActions() observeEventDisplayedActions()
observeSummaryState() observeSummaryState()
observeJumpToReadMarkerViewVisibility()
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
timeline.start() timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) } setState { copy(timeline = this@RoomDetailViewModel.timeline) }
@ -118,7 +123,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
when (action) { when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.SendMedia -> handleSendMedia(action) is RoomDetailActions.SendMedia -> handleSendMedia(action)
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) is RoomDetailActions.TimelineEventTurnsVisible -> handleEventVisible(action)
is RoomDetailActions.TimelineEventTurnsInvisible -> handleEventInvisible(action)
is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action) is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action)
is RoomDetailActions.SendReaction -> handleSendReaction(action) is RoomDetailActions.SendReaction -> handleSendReaction(action)
is RoomDetailActions.AcceptInvite -> handleAcceptInvite() is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
@ -136,10 +142,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailActions.RemoveFailedEcho -> handleRemove(action) is RoomDetailActions.RemoveFailedEcho -> handleRemove(action)
is RoomDetailActions.ClearSendQueue -> handleClearSendQueue() is RoomDetailActions.ClearSendQueue -> handleClearSendQueue()
is RoomDetailActions.ResendAll -> handleResendAll() is RoomDetailActions.ResendAll -> handleResendAll()
is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action)
is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead()
else -> Timber.e("Unhandled Action: $action") else -> Timber.e("Unhandled Action: $action")
} }
} }


private fun handleEventInvisible(action: RoomDetailActions.TimelineEventTurnsInvisible) {
invisibleEventsObservable.accept(action)
}

private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) {
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>() val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
?: return ?: return
@ -444,14 +456,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
room.sendMedias(attachments) room.sendMedias(attachments)
} }


private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { private fun handleEventVisible(action: RoomDetailActions.TimelineEventTurnsVisible) {
if (action.event.root.sendState.isSent()) { //ignore pending/local events if (action.event.root.sendState.isSent()) { //ignore pending/local events
displayedEventsObservable.accept(action) visibleEventsObservable.accept(action)
} }
//We need to update this with the related m.replace also (to move read receipt) //We need to update this with the related m.replace also (to move read receipt)
action.event.annotations?.editSummary?.sourceEvents?.forEach { action.event.annotations?.editSummary?.sourceEvents?.forEach {
room.getTimeLineEvent(it)?.let { event -> room.getTimeLineEvent(it)?.let { event ->
displayedEventsObservable.accept(RoomDetailActions.EventDisplayed(event)) visibleEventsObservable.accept(RoomDetailActions.TimelineEventTurnsVisible(event))
} }
} }
} }
@ -494,11 +506,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
} }


data class DownloadFileState(
val mimeType: String,
val file: File?,
val throwable: Throwable?
)


private fun handleDownloadFile(action: RoomDetailActions.DownloadFile) { private fun handleDownloadFile(action: RoomDetailActions.DownloadFile) {
session.downloadFile( session.downloadFile(
@ -530,53 +537,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro


private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) { private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) {
val targetEventId = action.eventId val targetEventId = action.eventId

val indexOfEvent = timeline.getIndexOfEvent(targetEventId)
if (action.position != null) { if (indexOfEvent == null) {
// Event is already in RAM // Event is not already in RAM
withState { timeline.restartWithEventId(targetEventId)
if (it.eventId == targetEventId) {
// ensure another click on the same permalink will also do a scroll
setState {
copy(
eventId = null
)
} }
if (action.highlight) {
setState { copy(highlightedEventId = targetEventId) }
} }

setState {
copy(
eventId = targetEventId
)
}
}

_navigateToEvent.postLiveEvent(targetEventId) _navigateToEvent.postLiveEvent(targetEventId)
} else {
// change timeline
timeline.dispose()
timeline = room.createTimeline(targetEventId, timelineSettings)
timeline.start()

withState {
if (it.eventId == targetEventId) {
// ensure another click on the same permalink will also do a scroll
setState {
copy(
eventId = null
)
}
}

setState {
copy(
eventId = targetEventId,
timeline = this@RoomDetailViewModel.timeline
)
}
}

_navigateToEvent.postLiveEvent(targetEventId)
}
} }


private fun handleResendEvent(action: RoomDetailActions.ResendMessage) { private fun handleResendEvent(action: RoomDetailActions.ResendMessage) {
@ -622,7 +591,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun observeEventDisplayedActions() { private fun observeEventDisplayedActions() {
// We are buffering scroll events for one second // We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on. // and keep the most recent one to set the read receipt on.
displayedEventsObservable visibleEventsObservable
.buffer(1, TimeUnit.SECONDS) .buffer(1, TimeUnit.SECONDS)
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.subscribeBy(onNext = { actions -> .subscribeBy(onNext = { actions ->
@ -634,6 +603,24 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
.disposeOnClear() .disposeOnClear()
} }


private fun handleSetReadMarkerAction(action: RoomDetailActions.SetReadMarkerAction) = withState { state ->
var readMarkerId = action.eventId
if (readMarkerId == state.asyncRoomSummary()?.readMarkerId) {
val indexOfEvent = timeline.getIndexOfEvent(action.eventId)
// force to set the read marker on the next event
if (indexOfEvent != null) {
timeline.getTimelineEventAtIndex(indexOfEvent - 1)?.root?.eventId?.also { eventIdOfNext ->
readMarkerId = eventIdOfNext
}
}
}
room.setReadMarker(readMarkerId, callback = object : MatrixCallback<Unit> {})
}

private fun handleMarkAllAsRead() {
room.markAllAsRead(object : MatrixCallback<Any> {})
}

private fun observeSyncState() { private fun observeSyncState() {
session.rx() session.rx()
.liveSyncState() .liveSyncState()
@ -645,6 +632,39 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
.disposeOnClear() .disposeOnClear()
} }


private fun observeJumpToReadMarkerViewVisibility() {
Observable
.combineLatest(
room.rx().liveRoomSummary(),
visibleEventsObservable.distinctUntilChanged(),
isEventVisibleObservable { it.hasReadMarker }.startWith(false),
Function3<RoomSummary, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { roomSummary, currentVisibleEvent, isReadMarkerViewVisible ->
val readMarkerId = roomSummary.readMarkerId
if (readMarkerId == null || isReadMarkerViewVisible || !timeline.isLive) {
false
} else {
val readMarkerPosition = timeline.getIndexOfEvent(readMarkerId)
?: Int.MAX_VALUE
val currentVisibleEventPosition = timeline.getIndexOfEvent(currentVisibleEvent.event.root.eventId)
?: Int.MIN_VALUE
readMarkerPosition > currentVisibleEventPosition
}
}
)
.distinctUntilChanged()
.subscribe {
setState { copy(showJumpToReadMarker = it) }
}
.disposeOnClear()
}

private fun isEventVisibleObservable(filterEvent: (TimelineEvent) -> Boolean): Observable<Boolean> {
return Observable.merge(
visibleEventsObservable.filter { filterEvent(it.event) }.map { true },
invisibleEventsObservable.filter { filterEvent(it.event) }.map { false }
)
}

private fun observeRoomSummary() { private fun observeRoomSummary() {
room.rx().liveRoomSummary() room.rx().liveRoomSummary()
.execute { async -> .execute { async ->

View File

@ -51,7 +51,9 @@ data class RoomDetailViewState(
val isEncrypted: Boolean = false, val isEncrypted: Boolean = false,
val tombstoneEvent: Event? = null, val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async<String> = Uninitialized, val tombstoneEventHandling: Async<String> = Uninitialized,
val syncState: SyncState = SyncState.IDLE val syncState: SyncState = SyncState.IDLE,
val showJumpToReadMarker: Boolean = false,
val highlightedEventId: String? = null
) : MvRxState { ) : MvRxState {


constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)

View File

@ -191,13 +191,12 @@ class RoomMessageTouchHelperCallback(private val context: Context,
} }


val y = (itemView.top + itemView.measuredHeight / 2).toFloat() val y = (itemView.top + itemView.measuredHeight / 2).toFloat()
val hw = imageDrawable.intrinsicWidth / 2f //magic numbers?
val hh = imageDrawable.intrinsicHeight / 2f
imageDrawable.setBounds( imageDrawable.setBounds(
(x - hw * scale).toInt(), (x - convertToPx(12) * scale).toInt(),
(y - hh * scale).toInt(), (y - convertToPx(11) * scale).toInt(),
(x + hw * scale).toInt(), (x + convertToPx(12) * scale).toInt(),
(y + hh * scale).toInt() (y + convertToPx(10) * scale).toInt()
) )
imageDrawable.draw(canvas) imageDrawable.draw(canvas)
imageDrawable.alpha = 255 imageDrawable.alpha = 255

View File

@ -38,7 +38,7 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa
// Do not scroll it item is already visible // Do not scroll it item is already visible
if (positionToScroll !in firstVisibleItem..lastVisibleItem) { if (positionToScroll !in firstVisibleItem..lastVisibleItem) {
// Note: Offset will be from the bottom, since the layoutManager is reversed // Note: Offset will be from the bottom, since the layoutManager is reversed
layoutManager.scrollToPositionWithOffset(positionToScroll, 120) layoutManager.scrollToPosition(position)
} }
scheduledEventId.set(null) scheduledEventId.set(null)
} }

View File

@ -69,7 +69,7 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL) LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration) epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
bottomSheetTitle.text = getString(R.string.read_at) bottomSheetTitle.text = getString(R.string.read_receipts_list)
epoxyController.setData(displayReadReceiptArgs.readReceipts) epoxyController.setData(displayReadReceiptArgs.readReceipts)
} }



View File

@ -49,11 +49,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
@TimelineEventControllerHandler @TimelineEventControllerHandler
private val backgroundHandler: Handler, private val backgroundHandler: Handler
userPreferencesProvider: UserPreferencesProvider
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {


interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback {
fun onEventInvisible(event: TimelineEvent)
fun onEventVisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent)
fun onRoomCreateLinkClicked(url: String) fun onRoomCreateLinkClicked(url: String)
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
@ -81,6 +81,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec


interface ReadReceiptsCallback { interface ReadReceiptsCallback {
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>) fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
fun onReadMarkerLongDisplayed(informationData: MessageInformationData)
} }


interface UrlClickCallback { interface UrlClickCallback {
@ -140,8 +141,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} }
} }


private val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()

init { init {
requestModelBuild() requestModelBuild()
} }
@ -247,7 +246,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec


private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): CacheItemData { private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
val event = items[currentPosition] val event = items[currentPosition]
val nextEvent = items.nextDisplayableEvent(currentPosition, showHiddenEvents) val nextEvent = items.nextOrNull(currentPosition)
val date = event.root.localDateTime() val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime() val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
@ -327,18 +326,42 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return shouldAdd return shouldAdd
} }


fun searchPositionOfEvent(eventId: String): Int? { fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) {
synchronized(modelCache) {
// Search in the cache // Search in the cache
modelCache.forEachIndexed { idx, cacheItemData -> var realPosition = 0
if (cacheItemData?.eventId == eventId) { for (i in 0 until modelCache.size) {
return idx val itemCache = modelCache[i]
if (itemCache?.eventId == eventId) {
return realPosition
}
if (itemCache?.eventModel != null) {
realPosition++
}
if (itemCache?.mergedHeaderModel != null) {
realPosition++
}
if (itemCache?.formattedDayModel != null) {
realPosition++
} }
} }

return null return null
} }

fun searchEventIdAtPosition(position: Int): String? = synchronized(modelCache) {
var offsetValue = 0
for (i in 0 until position) {
val itemCache = modelCache[i]
if (itemCache?.eventModel == null) {
offsetValue--
} }
if (itemCache?.mergedHeaderModel != null) {
offsetValue++
}
if (itemCache?.formattedDayModel != null) {
offsetValue++
}
}
return modelCache.getOrNull(position - offsetValue)?.eventId
} }


private data class CacheItemData( private data class CacheItemData(
@ -348,3 +371,5 @@ private data class CacheItemData(
val mergedHeaderModel: MergedHeaderItem? = null, val mergedHeaderModel: MergedHeaderItem? = null,
val formattedDayModel: DaySeparatorItem? = null val formattedDayModel: DaySeparatorItem? = null
) )

}

View File

@ -35,6 +35,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.extensions.canReact import im.vector.riotx.core.extensions.canReact
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.isSingleEmoji
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData




@ -243,7 +244,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false if (event.root.getClearType() != EventType.MESSAGE) return false
//TODO if user is admin or moderator //TODO if user is admin or moderator
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false return event.annotations?.reactionsSummary?.any { isSingleEmoji(it.key) } ?: false
} }





View File

@ -38,8 +38,12 @@ abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder<ReactionInfoSimpleI
@EpoxyAttribute @EpoxyAttribute
var timeStamp: CharSequence? = null var timeStamp: CharSequence? = null


@EpoxyAttribute
var emojiTypeFace: Typeface? = null

override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.emojiReactionView.text = reactionKey holder.emojiReactionView.text = reactionKey
holder.emojiReactionView.typeface = emojiTypeFace ?: Typeface.DEFAULT
holder.displayNameView.text = authorDisplayName holder.displayNameView.text = authorDisplayName
timeStamp?.let { timeStamp?.let {
holder.timeStampView.text = it holder.timeStampView.text = it

View File

@ -113,7 +113,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
when (it.operation) { when (it.operation) {
diff_match_patch.Operation.DELETE -> { diff_match_patch.Operation.DELETE -> {
span { span {
text = it.text.replace("\n"," ") text = it.text
textColor = ContextCompat.getColor(context, R.color.vector_error_color) textColor = ContextCompat.getColor(context, R.color.vector_error_color)
textDecorationLine = "line-through" textDecorationLine = "line-through"
} }

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