Merge branch 'develop' into feature/create_direct_room

This commit is contained in:
ganfra 2019-07-18 09:47:25 +02:00
commit 4341b0d0f5
95 changed files with 1106 additions and 694 deletions

View File

@ -2,19 +2,25 @@ Changes in RiotX 0.2.1 (2019-XX-XX)
=================================================== ===================================================


Features: Features:
- Message Editing: View edit history - Message Editing: View edit history (#121)
- Rooms filtering (#304) - Rooms filtering (#304)


Improvements: Improvements:
- Handle click on redacted events: view source and create permalink - Handle click on redacted events: view source and create permalink
- Improve long tap menu: reply on top, more compact (#368)
- Quick reply in timeline with swipe gesture (#167)
- Improve edit of replies
- Improve performance on Room Members and Users management (#381)


Other changes: Other changes:
- - migrate from rxbinding 2 to rxbinding 3


Bugfix: Bugfix:
- Fix regression on permalink click - Fix regression on permalink click
- Fix crash reported by the PlayStore (#341) - Fix crash reported by the PlayStore (#341)
- Fix Chat composer separator color in dark/black theme - Fix Chat composer separator color in dark/black theme
- Fix bad layout for room directory filter (#349)
- Fix Copying link from a message shouldn't open context menu (#364)


Translations: Translations:
- -

View File

@ -1,3 +1,5 @@
import javax.tools.JavaCompiler

// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.


buildscript { buildscript {
@ -52,6 +54,19 @@ allprojects {
} }
} }
} }

tasks.withType(JavaCompile).all {
options.compilerArgs += [
'-Adagger.gradle.incremental=enabled'
]
}

afterEvaluate {
extensions.findByName("kapt")?.arguments {
arg("dagger.gradle.incremental", "enabled")
}
}

} }


task clean(type: Delete) { task clean(type: Delete) {

View File

@ -99,14 +99,14 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"


implementation "androidx.appcompat:appcompat:1.1.0-beta01" implementation "androidx.appcompat:appcompat:1.1.0-rc01"
implementation "androidx.recyclerview:recyclerview:1.1.0-alpha06" implementation "androidx.recyclerview:recyclerview:1.1.0-beta01"


implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"


// Network // Network
implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0' implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
implementation 'com.squareup.okhttp3:okhttp:3.14.1' implementation 'com.squareup.okhttp3:okhttp:3.14.1'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'

View File

@ -57,6 +57,9 @@ interface Session :
*/ */
val sessionParams: SessionParams val sessionParams: SessionParams


/**
* Useful shortcut to get access to the userId
*/
val myUserId: String val myUserId: String
get() = sessionParams.credentials.userId get() = sessionParams.credentials.userId


@ -84,7 +87,7 @@ interface Session :
/** /**
* This method start the sync thread. * This method start the sync thread.
*/ */
fun startSync() fun startSync(fromForeground : Boolean)


/** /**
* This method stop the sync thread. * This method stop the sync thread.

View File

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




/** /**
* Constants defining known event relation types from Matrix specifications. * Constants defining known event relation types from Matrix specifications
*/ */
object RelationType { object RelationType {


@ -25,7 +25,7 @@ object RelationType {
const val ANNOTATION = "m.annotation" const val ANNOTATION = "m.annotation"
/** Lets you define an event which replaces an existing event.*/ /** Lets you define an event which replaces an existing event.*/
const val REPLACE = "m.replace" const val REPLACE = "m.replace"
/** ets you define an event which references an existing event.*/ /** Lets you define an event which references an existing event.*/
const val REFERENCE = "m.reference" const val REFERENCE = "m.reference"


} }

View File

@ -26,3 +26,8 @@ interface MessageContent {
val relatesTo: RelationDefaultContent? val relatesTo: RelationDefaultContent?
val newContent: Content? val newContent: Content?
} }


fun MessageContent?.isReply(): Boolean {
return this?.relatesTo?.inReplyTo != null
}

View File

@ -16,7 +16,10 @@


package im.vector.matrix.android.api.session.room.model.relation package im.vector.matrix.android.api.session.room.model.relation


import im.vector.matrix.android.api.session.events.model.RelationType

interface RelationContent { interface RelationContent {
/** See [RelationType] for known possible values */
val type: String? val type: String?
val eventId: String? val eventId: String?
val inReplyTo: ReplyToContent? val inReplyTo: ReplyToContent?

View File

@ -80,6 +80,22 @@ interface RelationService {
newBodyAutoMarkdown: Boolean, newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String = "* $newBodyText"): Cancelable compatibilityBodyText: String = "* $newBodyText"): Cancelable



/**
* Edit a reply. This is a special case because replies contains fallback text as a prefix.
* This method will take the new body (stripped from fallbacks) and re-add them before sending.
* @param replyToEdit The event to edit
* @param originalSenderId the sender of the message that this reply (being edited) is relating to
* @param originalEventId the event id that this reply (being edited) is relating to
* @param newBodyText The edited body (stripped from in reply to content)
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
*/
fun editReply(replyToEdit: TimelineEvent,
originalSenderId: String?,
originalEventId : String,
newBodyText: String,
compatibilityBodyText: String = "* $newBodyText"): Cancelable

/** /**
* Get's the edit history of the given event * Get's the edit history of the given event
*/ */

View File

@ -21,7 +21,9 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply


/** /**
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline. * This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
@ -88,3 +90,15 @@ data class TimelineEvent(
*/ */
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel() fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel() ?: root.getClearContent().toModel()


fun TimelineEvent.getTextEditableContent(): String? {
val originalContent = root.getClearContent().toModel<MessageContent>() ?: return null
val isReply = originalContent.isReply()
val lastContent = getLastMessageContent()
return if (isReply) {
return extractUsefulTextFromReply(lastContent?.body ?: "")
} else {
lastContent?.body ?: ""
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.util


object ContentUtils {
fun extractUsefulTextFromReply(repliedBody: String): String {
val lines = repliedBody.lines()
var wellFormed = repliedBody.startsWith(">")
var endOfPreviousFound = false
val usefullines = ArrayList<String>()
lines.forEach {
if (it == "") {
endOfPreviousFound = true
return@forEach
}
if (!endOfPreviousFound) {
wellFormed = wellFormed && it.startsWith(">")
} else {
usefullines.add(it)
}
}
return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
}

fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
if (repliedBody.startsWith("<mx-reply>")) {
val closingTagIndex = repliedBody.lastIndexOf("</mx-reply>")
if (closingTagIndex != -1)
return repliedBody.substring(closingTagIndex + "</mx-reply>".length).trim()
}
return repliedBody
}
}

View File

@ -50,16 +50,10 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
} }


private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent { private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
val userId = sessionParams.credentials.userId return sessionComponents.getOrPut(sessionParams.credentials.userId) {
if (sessionComponents.containsKey(userId)) { DaggerSessionComponent
return sessionComponents[userId]!!
}
return DaggerSessionComponent
.factory() .factory()
.create(matrixComponent, sessionParams) .create(matrixComponent, sessionParams)
.also {
sessionComponents[sessionParams.credentials.userId] = it
} }
} }

} }

View File

@ -21,7 +21,6 @@ package im.vector.matrix.android.internal.crypto
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.TextUtils
import arrow.core.Try import arrow.core.Try
import com.squareup.moshi.Types import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
@ -80,10 +79,9 @@ import im.vector.matrix.android.internal.util.fetchCopied
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import timber.log.Timber import timber.log.Timber
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.EmptyCoroutineContext import kotlin.math.max


/** /**
* A `CryptoService` class instance manages the end-to-end crypto for a session. * A `CryptoService` class instance manages the end-to-end crypto for a session.
@ -248,7 +246,7 @@ internal class CryptoManager @Inject constructor(
return return
} }
isStarting.set(true) isStarting.set(true)
CoroutineScope(coroutineDispatchers.crypto).launch { GlobalScope.launch(coroutineDispatchers.crypto) {
internalStart(isInitialSync) internalStart(isInitialSync)
} }
} }
@ -315,7 +313,7 @@ internal class CryptoManager @Inject constructor(
* @param syncResponse the syncResponse * @param syncResponse the syncResponse
*/ */
fun onSyncCompleted(syncResponse: SyncResponse) { fun onSyncCompleted(syncResponse: SyncResponse) {
CoroutineScope(coroutineDispatchers.crypto).launch { GlobalScope.launch(coroutineDispatchers.crypto) {
if (syncResponse.deviceLists != null) { if (syncResponse.deviceLists != null) {
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left) deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
} }
@ -340,7 +338,7 @@ internal class CryptoManager @Inject constructor(
* @return the device info, or null if not found / unsupported algorithm / crypto released * @return the device info, or null if not found / unsupported algorithm / crypto released
*/ */
override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? { override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? {
return if (!TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_MEGOLM) && !TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_OLM)) { return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) {
// We only deal in olm keys // We only deal in olm keys
null null
} else cryptoStore.deviceWithIdentityKey(senderKey) } else cryptoStore.deviceWithIdentityKey(senderKey)
@ -353,8 +351,8 @@ internal class CryptoManager @Inject constructor(
* @param deviceId the device id * @param deviceId the device id
*/ */
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? { override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
return if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) { return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
cryptoStore.getUserDevice(deviceId!!, userId) cryptoStore.getUserDevice(deviceId, userId)
} else { } else {
null null
} }
@ -439,7 +437,7 @@ internal class CryptoManager @Inject constructor(
// (for now at least. Maybe we should alert the user somehow?) // (for now at least. Maybe we should alert the user somehow?)
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)


if (!TextUtils.isEmpty(existingAlgorithm) && !TextUtils.equals(existingAlgorithm, algorithm)) { if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) {
Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
return false return false
} }
@ -535,7 +533,7 @@ internal class CryptoManager @Inject constructor(
eventType: String, eventType: String,
roomId: String, roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>) { callback: MatrixCallback<MXEncryptEventContentResult>) {
CoroutineScope(coroutineDispatchers.crypto).launch { GlobalScope.launch(coroutineDispatchers.crypto) {
if (!isStarted()) { if (!isStarted()) {
Timber.v("## encryptEventContent() : wait after e2e init") Timber.v("## encryptEventContent() : wait after e2e init")
internalStart(false) internalStart(false)
@ -601,7 +599,7 @@ internal class CryptoManager @Inject constructor(
* @param callback the callback to return data or null * @param callback the callback to return data or null
*/ */
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) { override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
GlobalScope.launch(EmptyCoroutineContext) { GlobalScope.launch {
val result = withContext(coroutineDispatchers.crypto) { val result = withContext(coroutineDispatchers.crypto) {
internalDecryptEvent(event, timeline) internalDecryptEvent(event, timeline)
} }
@ -649,7 +647,7 @@ internal class CryptoManager @Inject constructor(
* @param event the event * @param event the event
*/ */
fun onToDeviceEvent(event: Event) { fun onToDeviceEvent(event: Event) {
CoroutineScope(coroutineDispatchers.crypto).launch { GlobalScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) { when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
onRoomKeyEvent(event) onRoomKeyEvent(event)
@ -671,7 +669,7 @@ internal class CryptoManager @Inject constructor(
*/ */
private fun onRoomKeyEvent(event: Event) { private fun onRoomKeyEvent(event: Event) {
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.algorithm)) { if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
Timber.e("## onRoomKeyEvent() : missing fields") Timber.e("## onRoomKeyEvent() : missing fields")
return return
} }
@ -689,7 +687,7 @@ internal class CryptoManager @Inject constructor(
* @param event the encryption event. * @param event the encryption event.
*/ */
private fun onRoomEncryptionEvent(roomId: String, event: Event) { private fun onRoomEncryptionEvent(roomId: String, event: Event) {
CoroutineScope(coroutineDispatchers.crypto).launch { GlobalScope.launch(coroutineDispatchers.crypto) {
val params = LoadRoomMembersTask.Params(roomId) val params = LoadRoomMembersTask.Params(roomId)
loadRoomMembersTask loadRoomMembersTask
.execute(params) .execute(params)
@ -738,7 +736,7 @@ internal class CryptoManager @Inject constructor(
val membership = roomMember?.membership val membership = roomMember?.membership
if (membership == Membership.JOIN) { if (membership == Membership.JOIN) {
// make sure we are tracking the deviceList for this user. // make sure we are tracking the deviceList for this user.
deviceListManager.startTrackingDeviceList(Arrays.asList(userId)) deviceListManager.startTrackingDeviceList(listOf(userId))
} else if (membership == Membership.INVITE } else if (membership == Membership.INVITE
&& shouldEncryptForInvitedMembers(roomId) && shouldEncryptForInvitedMembers(roomId)
&& cryptoConfig.enableEncryptionForInvitedMembers) { && cryptoConfig.enableEncryptionForInvitedMembers) {
@ -747,7 +745,7 @@ internal class CryptoManager @Inject constructor(
// know what other servers are in the room at the time they've been invited. // know what other servers are in the room at the time they've been invited.
// They therefore will not send device updates if a user logs in whilst // They therefore will not send device updates if a user logs in whilst
// their state is invite. // their state is invite.
deviceListManager.startTrackingDeviceList(Arrays.asList(userId)) deviceListManager.startTrackingDeviceList(listOf(userId))
} }
} }
} }
@ -782,7 +780,11 @@ internal class CryptoManager @Inject constructor(
* @param callback the exported keys * @param callback the exported keys
*/ */
override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) { override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT, callback) GlobalScope.launch(coroutineDispatchers.main) {
runCatching {
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
}.fold(callback::onSuccess, callback::onFailure)
}
} }


/** /**
@ -792,31 +794,17 @@ internal class CryptoManager @Inject constructor(
* @param anIterationCount the encryption iteration count (0 means no encryption) * @param anIterationCount the encryption iteration count (0 means no encryption)
* @param callback the exported keys * @param callback the exported keys
*/ */
private fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) { private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray {
GlobalScope.launch(coroutineDispatchers.main) { return withContext(coroutineDispatchers.crypto) {
withContext(coroutineDispatchers.crypto) { val iterationCount = max(0, anIterationCount)
Try {
val iterationCount = Math.max(0, anIterationCount)


val exportedSessions = ArrayList<MegolmSessionData>() val exportedSessions = cryptoStore.getInboundGroupSessions().mapNotNull { it.exportKeys() }

val inboundGroupSessions = cryptoStore.getInboundGroupSessions()

for (session in inboundGroupSessions) {
val megolmSessionData = session.exportKeys()

if (null != megolmSessionData) {
exportedSessions.add(megolmSessionData)
}
}


val adapter = MoshiProvider.providesMoshi() val adapter = MoshiProvider.providesMoshi()
.adapter(List::class.java) .adapter(List::class.java)


MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount) MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
} }
}.foldToCallback(callback)
}
} }


/** /**
@ -879,7 +867,7 @@ internal class CryptoManager @Inject constructor(
*/ */
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) { fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
// force the refresh to ensure that the devices list is up-to-date // force the refresh to ensure that the devices list is up-to-date
CoroutineScope(coroutineDispatchers.crypto).launch { GlobalScope.launch(coroutineDispatchers.crypto) {
deviceListManager deviceListManager
.downloadKeys(userIds, true) .downloadKeys(userIds, true)
.fold( .fold(
@ -944,7 +932,7 @@ internal class CryptoManager @Inject constructor(
val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList() val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList()


if (add) { if (add) {
if (!roomIds.contains(roomId)) { if (roomId !in roomIds) {
roomIds.add(roomId) roomIds.add(roomId)
} }
} else { } else {
@ -1033,8 +1021,7 @@ internal class CryptoManager @Inject constructor(
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>() val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
val userIds = devicesInRoom.userIds val userIds = devicesInRoom.userIds
for (userId in userIds) { for (userId in userIds) {
val deviceIds = devicesInRoom.getUserDeviceIds(userId) devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId ->
deviceIds?.forEach { deviceId ->
devicesInRoom.getObject(userId, deviceId) devicesInRoom.getObject(userId, deviceId)
?.takeIf { it.isUnknown } ?.takeIf { it.isUnknown }
?.let { ?.let {
@ -1047,7 +1034,7 @@ internal class CryptoManager @Inject constructor(
} }


override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) { override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
CoroutineScope(coroutineDispatchers.crypto).launch { GlobalScope.launch(coroutineDispatchers.crypto) {
deviceListManager deviceListManager
.downloadKeys(userIds, forceDownload) .downloadKeys(userIds, forceDownload)
.foldToCallback(callback) .foldToCallback(callback)

View File

@ -29,7 +29,6 @@ import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevi
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
@ -38,10 +37,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.*
import kotlin.collections.HashMap import kotlin.collections.HashMap


internal class MXMegolmDecryption(private val credentials: Credentials, internal class MXMegolmDecryption(private val credentials: Credentials,
@ -312,7 +310,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
return return
} }
val userId = request.userId ?: return val userId = request.userId ?: return
CoroutineScope(coroutineDispatchers.crypto).launch { GlobalScope.launch(coroutineDispatchers.crypto) {
deviceListManager deviceListManager
.downloadKeys(listOf(userId), false) .downloadKeys(listOf(userId), false)
.flatMap { .flatMap {
@ -321,8 +319,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
if (deviceInfo == null) { if (deviceInfo == null) {
throw RuntimeException() throw RuntimeException()
} else { } else {
val devicesByUser = HashMap<String, List<MXDeviceInfo>>() val devicesByUser = mapOf(userId to listOf(deviceInfo))
devicesByUser[userId] = ArrayList(Arrays.asList(deviceInfo))
ensureOlmSessionsForDevicesAction ensureOlmSessionsForDevicesAction
.handle(devicesByUser) .handle(devicesByUser)
.flatMap { .flatMap {
@ -336,8 +333,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
Timber.v("## shareKeysWithDevice() : sharing keys for session" + Timber.v("## shareKeysWithDevice() : sharing keys for session" +
" ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId") " ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId")


val payloadJson = HashMap<String, Any>() val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
payloadJson["type"] = EventType.FORWARDED_ROOM_KEY


olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId) olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId)
.fold( .fold(
@ -350,7 +346,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
} }
) )


val encodedPayload = messageEncrypter.encryptMessage(payloadJson, Arrays.asList(deviceInfo)) val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>() val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload) sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId") Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId")

View File

@ -40,7 +40,7 @@ import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
@ -71,7 +71,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre


// Event received from the sync // Event received from the sync
fun onToDeviceEvent(event: Event) { fun onToDeviceEvent(event: Event) {
CoroutineScope(coroutineDispatchers.crypto).launch { GlobalScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) { when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> { EventType.KEY_VERIFICATION_START -> {
onStartRequestReceived(event) onStartRequestReceived(event)

View File

@ -16,7 +16,6 @@


package im.vector.matrix.android.internal.database.helper package im.vector.matrix.android.internal.database.helper


import androidx.annotation.VisibleForTesting
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.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.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
@ -103,7 +102,6 @@ internal fun ChunkEntity.updateSenderDataFor(eventIds: List<String>) {
} }
} }


@VisibleForTesting
internal fun ChunkEntity.add(roomId: String, internal fun ChunkEntity.add(roomId: String,
event: Event, event: Event,
direction: PaginationDirection, direction: PaginationDirection,

View File

@ -37,25 +37,22 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
} }
} }


internal fun RoomEntity.addStateEvents(stateEvents: List<Event>, internal fun RoomEntity.addStateEvent(stateEvent: Event,
stateIndex: Int = Int.MIN_VALUE, stateIndex: Int = Int.MIN_VALUE,
filterDuplicates: Boolean = false, filterDuplicates: Boolean = false,
isUnlinked: Boolean = false) { isUnlinked: Boolean = false) {
assertIsManaged() assertIsManaged()

if (stateEvent.eventId == null || (filterDuplicates && fastContains(stateEvent.eventId))) {
stateEvents.forEach { event -> return
if (event.eventId == null || (filterDuplicates && fastContains(event.eventId))) { } else {
return@forEach val entity = stateEvent.toEntity(roomId).apply {
}
val eventEntity = event.toEntity(roomId).apply {
this.stateIndex = stateIndex this.stateIndex = stateIndex
this.isUnlinked = isUnlinked this.isUnlinked = isUnlinked
this.sendState = SendState.SYNCED this.sendState = SendState.SYNCED
} }
untimelinedStateEvents.add(0, eventEntity) untimelinedStateEvents.add(entity)
} }
} }

internal fun RoomEntity.addSendingEvent(event: Event) { internal fun RoomEntity.addSendingEvent(event: Event) {
assertIsManaged() assertIsManaged()
val senderId = event.senderId ?: return val senderId = event.senderId ?: return

View File

@ -20,8 +20,6 @@ import io.realm.RealmObject
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.annotations.Index import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
import java.util.*




internal open class TimelineEventEntity(var localId: Long = 0, internal open class TimelineEventEntity(var localId: Long = 0,

View File

@ -19,21 +19,15 @@ package im.vector.matrix.android.internal.network
import arrow.core.Try import arrow.core.Try
import arrow.core.failure import arrow.core.failure
import arrow.core.recoverWith import arrow.core.recoverWith
import arrow.effects.IO
import arrow.effects.fix
import arrow.effects.instances.io.async.async
import arrow.integrations.retrofit.adapter.runAsync
import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Call import retrofit2.Call
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import kotlin.coroutines.resume


internal suspend inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute() internal suspend inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute()


@ -43,13 +37,8 @@ internal class Request<DATA> {
lateinit var apiCall: Call<DATA> lateinit var apiCall: Call<DATA>


suspend fun execute(): Try<DATA> { suspend fun execute(): Try<DATA> {
return suspendCancellableCoroutine { continuation -> return Try {
continuation.invokeOnCancellation { val response = apiCall.awaitResponse()
Timber.v("Request is canceled")
apiCall.cancel()
}
val result = Try {
val response = apiCall.runAsync(IO.async()).fix().unsafeRunSync()
if (response.isSuccessful) { if (response.isSuccessful) {
response.body() response.body()
?: throw IllegalStateException("The request returned a null body") ?: throw IllegalStateException("The request returned a null body")
@ -64,9 +53,6 @@ internal class Request<DATA> {
else -> Failure.Unknown(it) else -> Failure.Unknown(it)
}.failure() }.failure()
} }
continuation.resume(result)
}

} }


private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable { private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable {

View File

@ -0,0 +1,41 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/

package im.vector.matrix.android.internal.network

import kotlinx.coroutines.suspendCancellableCoroutine
import retrofit2.*
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

suspend fun <T> Call<T>.awaitResponse(): Response<T> {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
continuation.resume(response)
}

override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}

View File

@ -94,19 +94,21 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
} }


override fun requireBackgroundSync() { override fun requireBackgroundSync() {
SyncWorker.requireBackgroundSync(context, sessionParams.credentials.userId) SyncWorker.requireBackgroundSync(context, myUserId)
} }


override fun startAutomaticBackgroundSync(repeatDelay: Long) { override fun startAutomaticBackgroundSync(repeatDelay: Long) {
SyncWorker.automaticallyBackgroundSync(context, sessionParams.credentials.userId, 0, repeatDelay) SyncWorker.automaticallyBackgroundSync(context, myUserId, 0, repeatDelay)
} }


override fun stopAnyBackgroundSync() { override fun stopAnyBackgroundSync() {
SyncWorker.stopAnyBackgroundSync(context) SyncWorker.stopAnyBackgroundSync(context)
} }


override fun startSync() { override fun startSync(fromForeground : Boolean) {
Timber.i("Starting sync thread")
assert(isOpen) assert(isOpen)
syncThread.setInitialForeground(fromForeground)
if (!syncThread.isAlive) { if (!syncThread.isAlive) {
syncThread.start() syncThread.start()
} else { } else {

View File

@ -38,7 +38,6 @@ import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
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.prune.EventsPruner import im.vector.matrix.android.internal.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.session.user.UserEntityUpdater
import im.vector.matrix.android.internal.util.md5 import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -129,10 +128,6 @@ internal abstract class SessionModule {
@IntoSet @IntoSet
abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver


@Binds
@IntoSet
abstract fun bindUserEntityUpdater(groupSummaryUpdater: UserEntityUpdater): LiveEntityObserver

@Binds @Binds
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService



View File

@ -20,14 +20,13 @@ 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.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.Membership
import im.vector.matrix.android.api.session.room.model.RoomAvatarContent import im.vector.matrix.android.api.session.room.model.RoomAvatarContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import javax.inject.Inject import javax.inject.Inject


@ -42,32 +41,25 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona
fun resolve(roomId: String): String? { fun resolve(roomId: String): String? {
var res: String? = null var res: String? = null
monarchy.doWithRealm { realm -> monarchy.doWithRealm { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev()?.asDomain() val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev()?.asDomain()
res = roomName?.content.toModel<RoomAvatarContent>()?.avatarUrl res = roomName?.content.toModel<RoomAvatarContent>()?.avatarUrl
if (!res.isNullOrEmpty()) { if (!res.isNullOrEmpty()) {
return@doWithRealm return@doWithRealm
} }
val roomMembers = RoomMembers(realm, roomId) val roomMembers = RoomMembers(realm, roomId)
val members = roomMembers.getLoaded() val members = roomMembers.queryRoomMembersEvent().findAll()
if (roomEntity?.membership == Membership.INVITE) {
if (members.size == 1) {
res = members.entries.first().value.avatarUrl
} else if (members.size > 1) {
val firstOtherMember = members.filterKeys { it != credentials.userId }.values.firstOrNull()
res = firstOtherMember?.avatarUrl
}
} else {
// detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat)
if (members.size == 1) { if (members.size == 1) {
res = members.entries.first().value.avatarUrl res = members.firstOrNull()?.toRoomMember()?.avatarUrl
} else if (members.size == 2) { } else if (members.size == 2) {
val firstOtherMember = members.filterKeys { it != credentials.userId }.values.firstOrNull() val firstOtherMember = members.where().notEqualTo(EventEntityFields.STATE_KEY, credentials.userId).findFirst()
res = firstOtherMember?.avatarUrl res = firstOtherMember?.toRoomMember()?.avatarUrl
} }
} }

}
return res return res
} }

private fun EventEntity?.toRoomMember(): RoomMember? {
return this?.asDomain()?.content?.toModel<RoomMember>()
}
} }

View File

@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent
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.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.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.latestEvent import im.vector.matrix.android.internal.database.query.latestEvent
@ -86,12 +87,20 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C


val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includedTypes = PREVIEWABLE_TYPES) val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includedTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()
val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId }
val otherRoomMembers = RoomMembers(realm, roomId)
.queryRoomMembersEvent()
.notEqualTo(EventEntityFields.STATE_KEY, credentials.userId)
.findAll()
.asSequence()
.map { it.stateKey }

roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
roomSummaryEntity.latestEvent = latestEvent roomSummaryEntity.latestEvent = latestEvent
roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers.keys) roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)

} }
} }

View File

@ -17,9 +17,10 @@
package im.vector.matrix.android.internal.session.room.membership package im.vector.matrix.android.internal.session.room.membership


import arrow.core.Try import arrow.core.Try
import com.squareup.moshi.JsonReader
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
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.internal.database.helper.addStateEvents import im.vector.matrix.android.internal.database.helper.addStateEvent
import im.vector.matrix.android.internal.database.helper.updateSenderData import im.vector.matrix.android.internal.database.helper.updateSenderData
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
@ -27,10 +28,13 @@ import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomAPI
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.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionSync import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import okhttp3.ResponseBody
import okio.Okio
import javax.inject.Inject import javax.inject.Inject


internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolean> { internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolean> {
@ -60,23 +64,26 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP
} }
} }


private fun insertInDb(response: RoomMembersResponse, roomId: String): Try<RoomMembersResponse> { private fun insertInDb(response: RoomMembersResponse, roomId: String): Try<Unit> {
return monarchy return monarchy
.tryTransactionSync { realm -> .tryTransactionSync { realm ->
// We ignore all the already known members // We ignore all the already known members
val roomEntity = RoomEntity.where(realm, roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)


val roomMembers = RoomMembers(realm, roomId).getLoaded()
val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) } for (roomMemberEvent in response.roomMemberEvents) {
roomEntity.addStateEvents(eventsToInsert) roomEntity.addStateEvent(roomMemberEvent)
UserEntityFactory.createOrNull(roomMemberEvent)?.also {
realm.insertOrUpdate(it)
}
}
roomEntity.chunks.flatMap { it.timelineEvents }.forEach { roomEntity.chunks.flatMap { it.timelineEvents }.forEach {
it.updateSenderData() it.updateSenderData()
} }
roomEntity.areAllMembersLoaded = true roomEntity.areAllMembersLoaded = true
roomSummaryUpdater.update(realm, roomId) roomSummaryUpdater.update(realm, roomId)
} }
.map { response }
} }


private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { private fun areAllMembersAlreadyLoaded(roomId: String): Boolean {

View File

@ -25,13 +25,16 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomNameContent import im.vector.matrix.android.api.session.room.model.RoomNameContent
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.EventEntity 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.RoomEntity
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.query.prev import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import io.realm.RealmResults
import javax.inject.Inject import javax.inject.Inject


/** /**
@ -39,7 +42,6 @@ import javax.inject.Inject
*/ */
internal class RoomDisplayNameResolver @Inject constructor(private val context: Context, internal class RoomDisplayNameResolver @Inject constructor(private val context: Context,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val roomMemberDisplayNameResolver: RoomMemberDisplayNameResolver,
private val credentials: Credentials private val credentials: Credentials
) { ) {


@ -78,48 +80,61 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
} }


val roomMembers = RoomMembers(realm, roomId) val roomMembers = RoomMembers(realm, roomId)
val loadedMembers = roomMembers.getLoaded() val loadedMembers = roomMembers.queryRoomMembersEvent().findAll()
val otherRoomMembers = loadedMembers.filterKeys { it != credentials.userId } val otherMembersSubset = loadedMembers.where()
.notEqualTo(EventEntityFields.STATE_KEY, credentials.userId)
.limit(3)
.findAll()

if (roomEntity?.membership == Membership.INVITE) { if (roomEntity?.membership == Membership.INVITE) {
val inviteMeEvent = roomMembers.queryRoomMemberEvent(credentials.userId).findFirst() val inviteMeEvent = roomMembers.queryRoomMemberEvent(credentials.userId).findFirst()
val inviterId = inviteMeEvent?.sender val inviterId = inviteMeEvent?.sender
name = if (inviterId != null && otherRoomMembers.containsKey(inviterId)) { name = if (inviterId != null) {
roomMemberDisplayNameResolver.resolve(inviterId, otherRoomMembers) val inviterMemberEvent = loadedMembers.where()
.equalTo(EventEntityFields.STATE_KEY, inviterId)
.findFirst()
inviterMemberEvent?.toRoomMember()?.displayName
} else { } else {
context.getString(R.string.room_displayname_room_invite) context.getString(R.string.room_displayname_room_invite)
} }
} else { } else {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
val memberIds = if (roomSummary?.heroes?.isNotEmpty() == true) { val memberIds: List<String> = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes roomSummary.heroes
} else { } else {
otherRoomMembers.keys.toList() otherMembersSubset.mapNotNull { it.stateKey }
} }

name = when (memberIds.size) {
val nbOfOtherMembers = memberIds.size 0 -> context.getString(R.string.room_displayname_empty_room)

1 -> resolveRoomMember(otherMembersSubset[0], roomMembers)
when (nbOfOtherMembers) { 2 -> context.getString(R.string.room_displayname_two_members,
0 -> name = context.getString(R.string.room_displayname_empty_room) resolveRoomMember(otherMembersSubset[0], roomMembers),
1 -> name = roomMemberDisplayNameResolver.resolve(memberIds[0], otherRoomMembers) resolveRoomMember(otherMembersSubset[1], roomMembers)
2 -> {
val member1 = memberIds[0]
val member2 = memberIds[1]
name = context.getString(R.string.room_displayname_two_members,
roomMemberDisplayNameResolver.resolve(member1, otherRoomMembers),
roomMemberDisplayNameResolver.resolve(member2, otherRoomMembers)
) )
} else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
else -> {
val member = memberIds[0]
name = context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
roomMembers.getNumberOfJoinedMembers() - 1, roomMembers.getNumberOfJoinedMembers() - 1,
roomMemberDisplayNameResolver.resolve(member, otherRoomMembers), resolveRoomMember(otherMembersSubset[0], roomMembers),
roomMembers.getNumberOfJoinedMembers() - 1) roomMembers.getNumberOfJoinedMembers() - 1)
} }
} }
}
return@doWithRealm return@doWithRealm
} }
return name ?: roomId return name ?: roomId
} }

private fun resolveRoomMember(eventEntity: EventEntity?,
roomMembers: RoomMembers): String? {
if (eventEntity == null) return null
val roomMember = eventEntity.toRoomMember() ?: return null
val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName)
return if (isUnique) {
roomMember.displayName
} else {
"${roomMember.displayName} (${eventEntity.stateKey})"
}
}

private fun EventEntity?.toRoomMember(): RoomMember? {
return this?.asDomain()?.content?.toModel<RoomMember>()
}
} }

View File

@ -1,54 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.room.membership

import im.vector.matrix.android.api.session.room.model.RoomMember
import javax.inject.Inject

internal class RoomMemberDisplayNameResolver @Inject constructor() {

fun resolve(userId: String, members: Map<String, RoomMember>): String? {
val currentMember = members[userId]
var displayName = currentMember?.displayName
// Get the user display name from the member list of the room
// Do not consider null display name

if (currentMember != null && !currentMember.displayName.isNullOrEmpty()) {
val hasNameCollision = members
.filterValues { it != currentMember && it.displayName == currentMember.displayName }
.isNotEmpty()
if (hasNameCollision) {
displayName = "${currentMember.displayName} ( $userId )"
}
}

// TODO handle invited users
/*else if (null != member && TextUtils.equals(member!!.membership, RoomMember.MEMBERSHIP_INVITE)) {
val user = (mDataHandler as MXDataHandler).getUser(userId)
if (null != user) {
displayName = user!!.displayname
}
}
*/
if (displayName == null) {
// By default, use the user ID
displayName = userId
}
return displayName
}

}

View File

@ -33,6 +33,7 @@ import io.realm.Sort
* This class is an helper around STATE_ROOM_MEMBER events. * This class is an helper around STATE_ROOM_MEMBER events.
* It allows to get the live membership of a user. * It allows to get the live membership of a user.
*/ */

internal class RoomMembers(private val realm: Realm, internal class RoomMembers(private val realm: Realm,
private val roomId: String private val roomId: String
) { ) {
@ -72,27 +73,27 @@ internal class RoomMembers(private val realm: Realm,
.isNotNull(EventEntityFields.CONTENT) .isNotNull(EventEntityFields.CONTENT)
} }


fun queryJoinedRoomMembersEvent(): RealmQuery<EventEntity> {
return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"join\"")
}

fun queryInvitedRoomMembersEvent(): RealmQuery<EventEntity> {
return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"invite\"")
}

fun queryRoomMemberEvent(userId: String): RealmQuery<EventEntity> { fun queryRoomMemberEvent(userId: String): RealmQuery<EventEntity> {
return queryRoomMembersEvent() return queryRoomMembersEvent()
.equalTo(EventEntityFields.STATE_KEY, userId) .equalTo(EventEntityFields.STATE_KEY, userId)
} }


fun getLoaded(): Map<String, RoomMember> {
return queryRoomMembersEvent()
.findAll()
.map { it.asDomain() }
.associateBy { it.stateKey!! }
.mapValues { it.value.content.toModel<RoomMember>()!! }
}

fun getNumberOfJoinedMembers(): Int { fun getNumberOfJoinedMembers(): Int {
return roomSummary?.joinedMembersCount return roomSummary?.joinedMembersCount
?: getLoaded().filterValues { it.membership == Membership.JOIN }.size ?: queryJoinedRoomMembersEvent().findAll().size
} }


fun getNumberOfInvitedMembers(): Int { fun getNumberOfInvitedMembers(): Int {
return roomSummary?.invitedMembersCount return roomSummary?.invitedMembersCount
?: getLoaded().filterValues { it.membership == Membership.INVITE }.size ?: queryInvitedRoomMembersEvent().findAll().size
} }


fun getNumberOfMembers(): Int { fun getNumberOfMembers(): Int {

View File

@ -25,6 +25,7 @@ import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
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.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.model.relation.RelationService
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.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
@ -132,6 +133,24 @@ internal class DefaultRelationService @Inject constructor(private val context: C


} }


override fun editReply(replyToEdit: TimelineEvent,
originalSenderId: String?,
originalEventId: String,
newBodyText: String,
compatibilityBodyText: String): Cancelable {
val event = eventFactory
.createReplaceTextOfReply(roomId,
replyToEdit,
originalSenderId, originalEventId,
newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
.also {
saveLocalEcho(it)
}
val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
return CancelableWork(context, workRequest.id)
}

override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) { override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
val params = FetchEditHistoryTask.Params(roomId, eventId) val params = FetchEditHistoryTask.Params(roomId, eventId)
fetchEditHistoryTask.configureWith(params) fetchEditHistoryTask.configureWith(params)

View File

@ -104,6 +104,45 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
)) ))
} }


fun createReplaceTextOfReply(roomId: String, eventReplaced: TimelineEvent,
originalSenderId: String?,
originalEventId: String,
newBodyText: String,
newBodyAutoMarkdown: Boolean,
msgType: String,
compatibilityText: String): Event {
val permalink = PermalinkFactory.createPermalink(roomId, originalEventId)
val userLink = originalSenderId?.let { PermalinkFactory.createPermalink(it) } ?: ""

val body = bodyForReply(eventReplaced.getLastMessageContent(), eventReplaced.root.getClearContent().toModel())
val replyFormatted = REPLY_PATTERN.format(
permalink,
stringProvider.getString(R.string.message_reply_to_prefix),
userLink,
originalSenderId,
body.takeFormatted(),
createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
)
//
// > <@alice:example.org> This is the original body
//
val replyFallback = buildReplyFallback(body, originalSenderId, newBodyText)

return createEvent(roomId,
MessageTextContent(
type = msgType,
body = compatibilityText,
relatesTo = RelationDefaultContent(RelationType.REPLACE, eventReplaced.root.eventId),
newContent = MessageTextContent(
type = msgType,
format = MessageType.FORMAT_MATRIX_HTML,
body = replyFallback,
formattedBody = replyFormatted
)
.toContent()
))
}

fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event { fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event {
return when (attachment.type) { return when (attachment.type) {
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment) ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment)
@ -239,16 +278,8 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null
val userId = eventReplied.root.senderId ?: return null val userId = eventReplied.root.senderId ?: return null
val userLink = PermalinkFactory.createPermalink(userId) ?: return null val userLink = PermalinkFactory.createPermalink(userId) ?: return null
// <mx-reply>
// <blockquote> val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.root.getClearContent().toModel())
// <a href="https://matrix.to/#/!somewhere:domain.com/$event:domain.com">In reply to</a>
// <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a>
// <br />
// <!-- This is where the related event's HTML would be. -->
// </blockquote>
// </mx-reply>
// This is where the reply goes.
val body = bodyForReply(eventReplied.getLastMessageContent())
val replyFormatted = REPLY_PATTERN.format( val replyFormatted = REPLY_PATTERN.format(
permalink, permalink,
stringProvider.getString(R.string.message_reply_to_prefix), stringProvider.getString(R.string.message_reply_to_prefix),
@ -260,8 +291,22 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
// //
// > <@alice:example.org> This is the original body // > <@alice:example.org> This is the original body
// //
val replyFallback = buildReplyFallback(body, userId, replyText)

val eventId = eventReplied.root.eventId ?: return null
val content = MessageTextContent(
type = MessageType.MSGTYPE_TEXT,
format = MessageType.FORMAT_MATRIX_HTML,
body = replyFallback,
formattedBody = replyFormatted,
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
)
return createEvent(roomId, content)
}

private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {
val lines = body.text.split("\n") val lines = body.text.split("\n")
val replyFallback = StringBuffer("><$userId>") val replyFallback = StringBuffer("><$originalSenderId>")
lines.forEachIndexed { index, s -> lines.forEachIndexed { index, s ->
if (index == 0) { if (index == 0) {
replyFallback.append(" $s") replyFallback.append(" $s")
@ -269,23 +314,16 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
replyFallback.append("\n>$s") replyFallback.append("\n>$s")
} }
} }
replyFallback.append("\n\n").append(replyText) replyFallback.append("\n\n").append(newBodyText)

return replyFallback.toString()
val eventId = eventReplied.root.eventId ?: return null
val content = MessageTextContent(
type = MessageType.MSGTYPE_TEXT,
format = MessageType.FORMAT_MATRIX_HTML,
body = replyFallback.toString(),
formattedBody = replyFormatted,
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
)
return createEvent(roomId, content)
} }


/** /**
* Returns a TextContent used for the fallback event representation in a reply message. * Returns a TextContent used for the fallback event representation in a reply message.
* We also pass the original content, because in case of an edit of a reply the last content is not
* himself a reply, but it will contain the fallbacks, so we have to trim them.
*/ */
private fun bodyForReply(content: MessageContent?): TextContent { private fun bodyForReply(content: MessageContent?, originalContent: MessageContent?): TextContent {
when (content?.type) { when (content?.type) {
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
@ -296,7 +334,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
formattedText = content.formattedBody formattedText = content.formattedBody
} }
} }
val isReply = content.relatesTo?.inReplyTo?.eventId != null val isReply = content.isReply() || originalContent.isReply()
return if (isReply) return if (isReply)
TextContent(content.body, formattedText).removeInReplyFallbacks() TextContent(content.body, formattedText).removeInReplyFallbacks()
else else
@ -353,7 +391,16 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
companion object { companion object {
const val LOCAL_ID_PREFIX = "local." const val LOCAL_ID_PREFIX = "local."


// No whitespace
// <mx-reply>
// <blockquote>
// <a href="https://matrix.to/#/!somewhere:domain.com/$event:domain.com">In reply to</a>
// <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a>
// <br />
// <!-- This is where the related event's HTML would be. -->
// </blockquote>
// </mx-reply>
// No whitespace because currently breaks temporary formatted text to Span
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">%s</a><a href="%s">%s</a><br />%s</blockquote></mx-reply>%s""" const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">%s</a><a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""


fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(LOCAL_ID_PREFIX) fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(LOCAL_ID_PREFIX)

View File

@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.session.room.send


import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
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.util.ContentUtils.extractUsefulTextFromHtmlReply
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply


/** /**
* Contains a text and eventually a formatted text * Contains a text and eventually a formatted text
@ -47,28 +49,4 @@ fun TextContent.removeInReplyFallbacks(): TextContent {
) )
} }


private fun extractUsefulTextFromReply(repliedBody: String): String {
val lines = repliedBody.lines()
var wellFormed = repliedBody.startsWith(">")
var endOfPreviousFound = false
val usefullines = ArrayList<String>()
lines.forEach {
if (it == "") {
endOfPreviousFound = true
return@forEach
}
if (!endOfPreviousFound) {
wellFormed = wellFormed && it.startsWith(">")
} else {
usefullines.add(it)
}
}
return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
}


private fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
if (repliedBody.startsWith("<mx-reply>")) {
return repliedBody.substring(repliedBody.lastIndexOf("</mx-reply>") + "</mx-reply>".length).trim()
}
return repliedBody
}

View File

@ -43,7 +43,7 @@ import kotlin.collections.ArrayList
import kotlin.collections.HashMap import kotlin.collections.HashMap




private const val INITIAL_LOAD_SIZE = 10 private const val INITIAL_LOAD_SIZE = 30
private const val MIN_FETCHING_COUNT = 30 private const val MIN_FETCHING_COUNT = 30
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE



View File

@ -18,18 +18,14 @@ package im.vector.matrix.android.internal.session.room.timeline


import arrow.core.Try import arrow.core.Try
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvents
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.helper.isUnlinked
import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.create
import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import im.vector.matrix.android.internal.util.tryTransactionSync import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import timber.log.Timber import timber.log.Timber
@ -153,8 +149,14 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
currentChunk.isLastBackward = true currentChunk.isLastBackward = true
} else { } else {
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked()) val eventIds = ArrayList<String>(receivedChunk.events.size)

for (event in receivedChunk.events) {
event.eventId?.also { eventIds.add(it) }
currentChunk.add(roomId, event, direction, isUnlinked = currentChunk.isUnlinked())
UserEntityFactory.createOrNull(event)?.also {
realm.insertOrUpdate(it)
}
}
// Then we merge chunks if needed // Then we merge chunks if needed
if (currentChunk != prevChunk && prevChunk != null) { if (currentChunk != prevChunk && prevChunk != null) {
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk) currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
@ -170,7 +172,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
} }
} }
roomEntity.addOrUpdate(currentChunk) roomEntity.addOrUpdate(currentChunk)
roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = currentChunk.isUnlinked()) for (stateEvent in receivedChunk.stateEvents) {
roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked())
UserEntityFactory.createOrNull(stateEvent)?.also {
realm.insertOrUpdate(it)
}
}
currentChunk.updateSenderDataFor(eventIds)
} }
} }
.map { .map {

View File

@ -24,12 +24,11 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.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.CryptoManager import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvents
import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.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.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.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
@ -40,6 +39,7 @@ import im.vector.matrix.android.internal.session.notification.ProcessEventForPus
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.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.*
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
import io.realm.Realm import io.realm.Realm
@ -125,51 +125,31 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
} }
roomEntity.membership = Membership.JOIN roomEntity.membership = Membership.JOIN


val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
val isInitialSync = lastChunk == null
val lastStateIndex = lastChunk?.lastStateIndex(PaginationDirection.FORWARDS) ?: 0
val numberOfStateEvents = roomSync.state?.events?.size ?: 0
val stateIndexOffset = lastStateIndex + numberOfStateEvents

// State event // State event
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) { if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
val untimelinedStateIndex = if (isInitialSync) Int.MIN_VALUE else stateIndexOffset val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
roomEntity.addStateEvents(roomSync.state.events, filterDuplicates = true, stateIndex = untimelinedStateIndex) ?: Int.MIN_VALUE

val untimelinedStateIndex = minStateIndex + 1
roomSync.state.events.forEach { event ->
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
// Give info to crypto module // Give info to crypto module
roomSync.state.events.forEach { cryptoManager.onStateEvent(roomId, event)
cryptoManager.onStateEvent(roomId, it) UserEntityFactory.createOrNull(event)?.also {
realm.insertOrUpdate(it)
}
} }
} }


if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) { if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
val timelineStateOffset = if (isInitialSync || roomSync.timeline.limited.not()) 0 else stateIndexOffset
val chunkEntity = handleTimelineEvents( val chunkEntity = handleTimelineEvents(
realm, realm,
roomId, roomEntity,
roomSync.timeline.events, roomSync.timeline.events,
roomSync.timeline.prevToken, roomSync.timeline.prevToken,
roomSync.timeline.limited, roomSync.timeline.limited,
timelineStateOffset 0
) )
roomEntity.addOrUpdate(chunkEntity) roomEntity.addOrUpdate(chunkEntity)

// Give info to crypto module
roomSync.timeline.events.forEach {
cryptoManager.onLiveEvent(roomId, it)
}

// Try to remove local echo
val transactionIds = roomSync.timeline.events.mapNotNull { it.unsignedData?.transactionId }
transactionIds.forEach {
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
if (sendingEventEntity != null) {
Timber.v("Remove local echo for tx:$it")
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
} else {
Timber.v("Can't find corresponding local echo for tx:$it")
}
}
} }
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications) roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)


@ -192,7 +172,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
?: realm.createObject(roomId) ?: 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, roomId, roomSync.inviteState.events) val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
roomEntity.addOrUpdate(chunkEntity) roomEntity.addOrUpdate(chunkEntity)
} }
roomSummaryUpdater.update(realm, roomId, Membership.INVITE) roomSummaryUpdater.update(realm, roomId, Membership.INVITE)
@ -212,13 +192,13 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
} }


private fun handleTimelineEvents(realm: Realm, private fun handleTimelineEvents(realm: Realm,
roomId: String, roomEntity: RoomEntity,
eventList: List<Event>, eventList: List<Event>,
prevToken: String? = null, prevToken: String? = null,
isLimited: Boolean = true, isLimited: Boolean = true,
stateIndexOffset: Int = 0): ChunkEntity { stateIndexOffset: Int = 0): ChunkEntity {


val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
val chunkEntity = if (!isLimited && lastChunk != null) { val chunkEntity = if (!isLimited && lastChunk != null) {
lastChunk lastChunk
} else { } else {
@ -226,13 +206,32 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
} }
lastChunk?.isLastForward = false lastChunk?.isLastForward = false
chunkEntity.isLastForward = true chunkEntity.isLastForward = true
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)

//update eventAnnotationSummary here?


val eventIds = ArrayList<String>(eventList.size)
for (event in eventList) {
event.eventId?.also { eventIds.add(it) }
chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)
// Give info to crypto module
cryptoManager.onLiveEvent(roomEntity.roomId, event)
// Try to remove local echo
event.unsignedData?.transactionId?.also {
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
if (sendingEventEntity != null) {
Timber.v("Remove local echo for tx:$it")
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
} else {
Timber.v("Can't find corresponding local echo for tx:$it")
}
}
UserEntityFactory.createOrNull(event)?.also {
realm.insertOrUpdate(it)
}
}
chunkEntity.updateSenderDataFor(eventIds)
return chunkEntity return chunkEntity
} }



private fun handleEphemeral(realm: Realm, private fun handleEphemeral(realm: Realm,
roomId: String, roomId: String,
ephemeral: RoomSyncEphemeral) { ephemeral: RoomSyncEphemeral) {

View File

@ -20,8 +20,6 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.user.DefaultUpdateUserTask
import im.vector.matrix.android.internal.session.user.UpdateUserTask
import retrofit2.Retrofit import retrofit2.Retrofit


@Module @Module

View File

@ -54,6 +54,10 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
updateStateTo(SyncState.IDLE) updateStateTo(SyncState.IDLE)
} }


fun setInitialForeground(initialForground : Boolean) {
updateStateTo(if (initialForground) SyncState.IDLE else SyncState.PAUSED)
}

fun restart() = synchronized(lock) { fun restart() = synchronized(lock) {
if (state is SyncState.PAUSED) { if (state is SyncState.PAUSED) {
Timber.v("Resume sync...") Timber.v("Resume sync...")
@ -84,7 +88,6 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
Timber.v("Start syncing...") Timber.v("Start syncing...")
networkConnectivityChecker.register(this) networkConnectivityChecker.register(this)
backgroundDetectionObserver.register(this) backgroundDetectionObserver.register(this)
updateStateTo(SyncState.RUNNING(catchingUp = true))


while (state != SyncState.KILLING) { while (state != SyncState.KILLING) {
if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) { if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) {
@ -93,7 +96,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
lock.wait() lock.wait()
} }
} else { } else {
Timber.v("Execute sync request with timeout $DEFAULT_LONG_POOL_TIMEOUT") updateStateTo(SyncState.RUNNING(catchingUp = true))
Timber.v("[$this] Execute sync request with timeout $DEFAULT_LONG_POOL_TIMEOUT")
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
val params = SyncTask.Params(DEFAULT_LONG_POOL_TIMEOUT) val params = SyncTask.Params(DEFAULT_LONG_POOL_TIMEOUT)
cancelableTask = syncTask.configureWith(params) cancelableTask = syncTask.configureWith(params)
@ -148,6 +152,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
} }


private fun updateStateTo(newState: SyncState) { private fun updateStateTo(newState: SyncState) {
Timber.v("Update state to $newState")
state = newState state = newState
liveState.postValue(newState) liveState.postValue(newState)
} }

View File

@ -1,58 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.user

import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionSync
import javax.inject.Inject

internal interface UpdateUserTask : Task<UpdateUserTask.Params, Unit> {

data class Params(val eventIds: List<String>)

}

internal class DefaultUpdateUserTask @Inject constructor(private val monarchy: Monarchy) : UpdateUserTask {

override suspend fun execute(params: UpdateUserTask.Params): Try<Unit> {
return monarchy.tryTransactionSync { realm ->
params.eventIds.forEach { eventId ->
val event = EventEntity.where(realm, eventId).findFirst()?.asDomain()
?: return@forEach
val roomId = event.roomId ?: return@forEach
val userId = event.stateKey ?: return@forEach
val roomMember = RoomMembers(realm, roomId).get(userId) ?: return@forEach
if (roomMember.membership != Membership.JOIN) return@forEach

val userEntity = UserEntity.where(realm, userId).findFirst()
?: realm.createObject(UserEntity::class.java, userId)
userEntity.displayName = roomMember.displayName ?: ""
userEntity.avatarUrl = roomMember.avatarUrl ?: ""
}
}
}

}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.user

import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.model.UserEntity

internal object UserEntityFactory {

fun createOrNull(event: Event): UserEntity? {
if (event.type != EventType.STATE_ROOM_MEMBER) {
return null
}
val roomMember = event.content.toModel<RoomMember>() ?: return null
return UserEntity(event.stateKey ?: "",
roomMember.displayName ?: "",
roomMember.avatarUrl ?: ""
)
}


}

View File

@ -1,60 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.user

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmConfiguration
import io.realm.RealmResults
import io.realm.Sort
import javax.inject.Inject

internal class UserEntityUpdater @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
private val updateUserTask: UpdateUserTask,
private val taskExecutor: TaskExecutor)
: RealmLiveEntityObserver<EventEntity>(realmConfiguration) {

override val query = Monarchy.Query<EventEntity> {
EventEntity
.types(it, listOf(EventType.STATE_ROOM_MEMBER))
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.distinct(EventEntityFields.STATE_KEY)
}

override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
val roomMembersEvents = changeSet.insertions
.asSequence()
.mapNotNull { results[it]?.eventId }
.toList()

val taskParams = UpdateUserTask.Params(roomMembersEvents)
updateUserTask
.configureWith(taskParams)
.executeOn(TaskThread.IO)
.executeBy(taskExecutor)
}

}

View File

@ -26,7 +26,4 @@ internal abstract class UserModule {
@Binds @Binds
abstract fun bindUserService(userService: DefaultUserService): UserService abstract fun bindUserService(userService: DefaultUserService): UserService


@Binds
abstract fun bindUpdateUserTask(updateUserTask: DefaultUpdateUserTask): UpdateUserTask

} }

View File

@ -187,8 +187,9 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.0' implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.0'
// TODO RxBindings3 exists // RXBinding
implementation 'com.jakewharton.rxbinding2:rxbinding:2.2.0' implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0-alpha2'
implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0-alpha2'


implementation("com.airbnb.android:epoxy:$epoxy_version") implementation("com.airbnb.android:epoxy:$epoxy_version")
kapt "com.airbnb.android:epoxy-processor:$epoxy_version" kapt "com.airbnb.android:epoxy-processor:$epoxy_version"

View File

@ -199,7 +199,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
if (eventType == null) { if (eventType == null) {
//Just add a generic unknown event //Just add a generic unknown event
val simpleNotifiableEvent = SimpleNotifiableEvent( val simpleNotifiableEvent = SimpleNotifiableEvent(
session.sessionParams.credentials.userId, session.myUserId,
eventId, eventId,
true, //It's an issue in this case, all event will bing even if expected to be silent. true, //It's an issue in this case, all event will bing even if expected to be silent.
title = getString(R.string.notification_unknown_new_event), title = getString(R.string.notification_unknown_new_event),
@ -238,7 +238,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
} }


notifiableEvent.isPushGatewayEvent = true notifiableEvent.isPushGatewayEvent = true
notifiableEvent.matrixID = session.sessionParams.credentials.userId notifiableEvent.matrixID = session.myUserId
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent) notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
notificationDrawerManager.refreshNotificationDrawer() notificationDrawerManager.refreshNotificationDrawer()
} }

View File

@ -59,9 +59,15 @@ import im.vector.riotx.features.workers.signout.SignOutViewModel
interface ViewModelModule { interface ViewModelModule {




/**
* ViewModels with @IntoMap will be injected by this factory
*/
@Binds @Binds
fun bindViewModelFactory(factory: VectorViewModelFactory): ViewModelProvider.Factory fun bindViewModelFactory(factory: VectorViewModelFactory): ViewModelProvider.Factory


/**
* Below are bindings for the androidx view models (which extend ViewModel). Will be converted to MvRx ViewModel in the future.
*/
@Binds @Binds
@IntoMap @IntoMap
@ViewModelKey(SignOutViewModel::class) @ViewModelKey(SignOutViewModel::class)
@ -112,6 +118,10 @@ interface ViewModelModule {
@ViewModelKey(ConfigurationViewModel::class) @ViewModelKey(ConfigurationViewModel::class)
fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel


/**
* Below are bindings for the MvRx view models (which extend VectorViewModel). Will be the only usage in the future.
*/

@Binds @Binds
fun bindHomeActivityViewModelFactory(factory: HomeActivityViewModel_AssistedFactory): HomeActivityViewModel.Factory fun bindHomeActivityViewModelFactory(factory: HomeActivityViewModel_AssistedFactory): HomeActivityViewModel.Factory



View File

@ -16,14 +16,20 @@


package im.vector.riotx.core.extensions package im.vector.riotx.core.extensions


import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.notifications.PushRuleTriggerListener
import timber.log.Timber


fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener) { fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener) {
open() open()
setFilter(FilterService.FilterPreset.RiotFilter) setFilter(FilterService.FilterPreset.RiotFilter)
startSync() Timber.i("Configure and start session for ${this.myUserId}")
val isAtLeastStarted = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
Timber.v("--> is at least started? $isAtLeastStarted")
startSync(isAtLeastStarted)
refreshPushers() refreshPushers()
pushRuleTriggerListener.startWithSession(this) pushRuleTriggerListener.startWithSession(this)
fetchPushRules() fetchPushRules()

View File

@ -18,7 +18,18 @@ package im.vector.riotx.core.platform


import com.airbnb.mvrx.BaseMvRxViewModel import com.airbnb.mvrx.BaseMvRxViewModel
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig


abstract class VectorViewModel<S : MvRxState>(initialState: S) abstract class VectorViewModel<S : MvRxState>(initialState: S)
: BaseMvRxViewModel<S>(initialState, false) : BaseMvRxViewModel<S>(initialState, false) {

protected val cancelableBag = CancelableBag()

override fun onCleared() {
super.onCleared()
cancelableBag.cancel()
}


}

View File

@ -58,10 +58,10 @@ open class UserAvatarPreference : Preference {
open fun refreshAvatar() { open fun refreshAvatar() {
val session = mSession ?: return val session = mSession ?: return
val view = mAvatarView ?: return val view = mAvatarView ?: return
session.getUser(session.sessionParams.credentials.userId)?.let { session.getUser(session.myUserId)?.let {
avatarRenderer.render(it, view) avatarRenderer.render(it, view)
} ?: run { } ?: run {
avatarRenderer.render(null, session.sessionParams.credentials.userId, null, view) avatarRenderer.render(null, session.myUserId, null, view)
} }


} }

View File

@ -28,6 +28,7 @@ import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.widget.Toast import android.widget.Toast
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotx.R import im.vector.riotx.R
@ -81,11 +82,11 @@ fun requestDisablingBatteryOptimization(activity: Activity, fragment: Fragment?,
* @param context the context * @param context the context
* @param text the text to copy * @param text the text to copy
*/ */
fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = true) { fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = true, @StringRes toastMessage : Int = R.string.copied_to_clipboard) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText("", text) clipboard.primaryClip = ClipData.newPlainText("", text)
if (showToast) { if (showToast) {
context.toast(R.string.copied_to_clipboard) context.toast(toastMessage)
} }
} }



View File

@ -21,8 +21,8 @@ import androidx.lifecycle.ViewModel
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.StepProgressListener import im.vector.matrix.android.api.listeners.StepProgressListener
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.core.ui.views.KeysBackupBanner import im.vector.riotx.core.ui.views.KeysBackupBanner
@ -57,7 +57,7 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor() : ViewModel() {
keysBackup.restoreKeysWithRecoveryKey(keysVersionResult, keysBackup.restoreKeysWithRecoveryKey(keysVersionResult,
recoveryKey, recoveryKey,
null, null,
session.sessionParams.credentials.userId, session.myUserId,
object : StepProgressListener { object : StepProgressListener {
override fun onStepProgress(step: StepProgressListener.Step) { override fun onStepProgress(step: StepProgressListener.Step) {
when (step) { when (step) {

View File

@ -21,8 +21,8 @@ import androidx.lifecycle.ViewModel
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.StepProgressListener import im.vector.matrix.android.api.listeners.StepProgressListener
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.core.ui.views.KeysBackupBanner import im.vector.riotx.core.ui.views.KeysBackupBanner
@ -58,7 +58,7 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor() : ViewModel
keysBackup.restoreKeyBackupWithPassword(keysVersionResult, keysBackup.restoreKeyBackupWithPassword(keysVersionResult,
passphrase.value!!, passphrase.value!!,
null, null,
sharedViewModel.session.sessionParams.credentials.userId, sharedViewModel.session.myUserId,
object : StepProgressListener { object : StepProgressListener {
override fun onStepProgress(step: StepProgressListener.Step) { override fun onStepProgress(step: StepProgressListener.Step) {
when (step) { when (step) {

View File

@ -51,8 +51,7 @@ class HomeDrawerFragment : VectorBaseFragment() {
val groupListFragment = GroupListFragment.newInstance() val groupListFragment = GroupListFragment.newInstance()
replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer) replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer)
} }

session.liveUser(session.myUserId).observeK(this) { user ->
session.liveUser(session.sessionParams.credentials.userId).observeK(this) { user ->
if (user != null) { if (user != null) {
avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView) avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView)
homeDrawerUsernameView.text = user.displayName homeDrawerUsernameView.text = user.displayName

View File

@ -93,7 +93,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
.rx() .rx()
.liveGroupSummaries() .liveGroupSummaries()
.map { .map {
val myUser = session.getUser(session.sessionParams.credentials.userId) val myUser = session.getUser(session.myUserId)
val allCommunityGroup = GroupSummary( val allCommunityGroup = GroupSummary(
groupId = ALL_COMMUNITIES_GROUP_ID, groupId = ALL_COMMUNITIES_GROUP_ID,
displayName = stringProvider.getString(R.string.group_all_communities), displayName = stringProvider.getString(R.string.group_all_communities),

View File

@ -37,9 +37,11 @@ import androidx.annotation.DrawableRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
@ -57,8 +59,10 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
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.*
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
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
@ -87,7 +91,7 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
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.*
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.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.*
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
@ -258,7 +262,7 @@ class RoomDetailFragment :
composerLayout.composerRelatedMessageContent.text = formattedBody composerLayout.composerRelatedMessageContent.text = formattedBody
?: nonFormattedBody ?: nonFormattedBody


composerLayout.composerEditText.setText(if (useText) nonFormattedBody else "") composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))


avatarRenderer.render(event.senderAvatar, event.root.senderId avatarRenderer.render(event.senderAvatar, event.root.senderId
@ -323,6 +327,32 @@ class RoomDetailFragment :
}) })
recyclerView.setController(timelineEventController) recyclerView.setController(timelineEventController)
timelineEventController.callback = this timelineEventController.callback = this

if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) {
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
R.drawable.ic_reply,
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
(model as? AbsMessageItem)?.informationData?.let {
val eventId = it.eventId
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
}
}

override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
return when (model) {
is MessageFileItem,
is MessageImageVideoItem,
is MessageTextItem -> {
return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
}
else -> false
}
}
})
val touchHelper = ItemTouchHelper(swipeCallback)
touchHelper.attachToRecyclerView(recyclerView)
}
} }


private fun setupComposer() { private fun setupComposer() {
@ -486,7 +516,7 @@ class RoomDetailFragment :
timelineEventController.setTimeline(state.timeline, state.eventId) timelineEventController.setTimeline(state.timeline, state.eventId)
inviteView.visibility = View.GONE inviteView.visibility = View.GONE


val uid = session.sessionParams.credentials.userId val uid = session.myUserId
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView) avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)


@ -572,7 +602,7 @@ class RoomDetailFragment :


override fun onUrlLongClicked(url: String): Boolean { override fun onUrlLongClicked(url: String): Boolean {
// Copy the url to the clipboard // Copy the url to the clipboard
copyToClipboard(requireContext(), url) copyToClipboard(requireContext(), url, true, R.string.link_copied_to_clipboard)
return true return true
} }


@ -780,7 +810,7 @@ class RoomDetailFragment :
if (null != text) { if (null != text) {
// var vibrate = false // var vibrate = false


val myDisplayName = session.getUser(session.sessionParams.credentials.userId)?.displayName val myDisplayName = session.getUser(session.myUserId)?.displayName
if (TextUtils.equals(myDisplayName, text)) { if (TextUtils.equals(myDisplayName, text)) {
// current user // current user
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
@ -828,10 +858,12 @@ class RoomDetailFragment :
// VectorInviteView.Callback // VectorInviteView.Callback


override fun onAcceptInvite() { override fun onAcceptInvite() {
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
roomDetailViewModel.process(RoomDetailActions.AcceptInvite) roomDetailViewModel.process(RoomDetailActions.AcceptInvite)
} }


override fun onRejectInvite() { override fun onRejectInvite() {
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
roomDetailViewModel.process(RoomDetailActions.RejectInvite) roomDetailViewModel.process(RoomDetailActions.RejectInvite)
} }
} }

View File

@ -39,7 +39,6 @@ import im.vector.matrix.android.api.session.room.model.message.getFileUrl
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.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.R
import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.resources.UserPreferencesProvider
@ -52,8 +51,6 @@ import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit




@ -97,7 +94,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
observeRoomSummary() observeRoomSummary()
observeEventDisplayedActions() observeEventDisplayedActions()
observeInvitationState() observeInvitationState()
room.loadRoomMembersIfNeeded() cancelableBag += room.loadRoomMembersIfNeeded()
timeline.start() timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) } setState { copy(timeline = this@RoomDetailViewModel.timeline) }
} }
@ -229,17 +226,25 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
} }
is SendMode.EDIT -> { is SendMode.EDIT -> {

//is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
if (inReplyTo != null) {
//TODO check if same content?
room.editReply(state.sendMode.timelineEvent, room.getTimeLineEvent(inReplyTo)?.root?.senderId, inReplyTo, action.text)
} else {
val messageContent: MessageContent? = val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel() ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val nonFormattedBody = messageContent?.body ?: "" val existingBody = messageContent?.body ?: ""

if (existingBody != action.text) {
if (nonFormattedBody != action.text) {
room.editTextMessage(state.sendMode.timelineEvent.root.eventId room.editTextMessage(state.sendMode.timelineEvent.root.eventId
?: "", messageContent?.type ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) ?: "", messageContent?.type
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
} else { } else {
Timber.w("Same message content, do not send edition") Timber.w("Same message content, do not send edition")
} }
}
setState { setState {
copy( copy(
sendMode = SendMode.REGULAR sendMode = SendMode.REGULAR
@ -347,7 +352,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }


private fun handleUndoReact(action: RoomDetailActions.UndoReaction) { private fun handleUndoReact(action: RoomDetailActions.UndoReaction) {
room.undoReaction(action.key, action.targetEventId, session.sessionParams.credentials.userId) room.undoReaction(action.key, action.targetEventId, session.myUserId)
} }




@ -355,7 +360,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
if (action.add) { if (action.add) {
room.sendReaction(action.selectedReaction, action.targetEventId) room.sendReaction(action.selectedReaction, action.targetEventId)
} else { } else {
room.undoReaction(action.selectedReaction, action.targetEventId, session.sessionParams.credentials.userId) room.undoReaction(action.selectedReaction, action.targetEventId, session.myUserId)
} }
} }



View File

@ -0,0 +1,214 @@
/*
* 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 android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.util.TypedValue
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_SWIPE
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyTouchHelperCallback
import com.airbnb.epoxy.EpoxyViewHolder
import timber.log.Timber


class RoomMessageTouchHelperCallback(private val context: Context,
@DrawableRes actionIcon: Int,
private val handler: QuickReplayHandler) : EpoxyTouchHelperCallback() {

interface QuickReplayHandler {
fun performQuickReplyOnHolder(model: EpoxyModel<*>)
fun canSwipeModel(model: EpoxyModel<*>): Boolean
}

private var swipeBack: Boolean = false
private var dX = 0f
private var startTracking = false
private var isVibrate = false

private var replyButtonProgress: Float = 0F
private var lastReplyButtonAnimationTime: Long = 0

private var imageDrawable: Drawable = ContextCompat.getDrawable(context, actionIcon)!!


private val triggerDistance = convertToPx(100)
private val minShowDistance = convertToPx(20)
private val triggerDelta = convertToPx(20)

override fun onSwiped(viewHolder: EpoxyViewHolder?, direction: Int) {

}

override fun onMove(recyclerView: RecyclerView?, viewHolder: EpoxyViewHolder?, target: EpoxyViewHolder?): Boolean {
return false
}

override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: EpoxyViewHolder): Int {
if (handler.canSwipeModel(viewHolder.model)) {
return ItemTouchHelper.Callback.makeMovementFlags(0, ItemTouchHelper.START) //Should we use Left?
} else {
return 0
}
}


//We never let items completely go out
override fun convertToAbsoluteDirection(flags: Int, layoutDirection: Int): Int {
if (swipeBack) {
swipeBack = false
return 0
}
return super.convertToAbsoluteDirection(flags, layoutDirection)
}

override fun onChildDraw(c: Canvas,
recyclerView: RecyclerView,
viewHolder: EpoxyViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean) {
if (actionState == ACTION_STATE_SWIPE) {
setTouchListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
val size = triggerDistance
if (Math.abs(viewHolder.itemView.translationX) < size || dX > this.dX /*going back*/) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
this.dX = dX
startTracking = true
}
drawReplyButton(c, viewHolder.itemView)
}


@SuppressLint("ClickableViewAccessibility")
private fun setTouchListener(c: Canvas,
recyclerView: RecyclerView,
viewHolder: EpoxyViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean) {
//TODO can this interfer with other interactions? should i remove it
recyclerView.setOnTouchListener { v, event ->
swipeBack = event.action == MotionEvent.ACTION_CANCEL || event.action == MotionEvent.ACTION_UP
if (swipeBack) {
if (Math.abs(dX) >= triggerDistance) {
try {
viewHolder.model?.let { handler.performQuickReplyOnHolder(it) }
} catch (e: IllegalStateException) {
Timber.e(e)
}
}
}
false
}
}


private fun drawReplyButton(canvas: Canvas, itemView: View) {

Timber.v("drawReplyButton")
val translationX = Math.abs(itemView.translationX)
val newTime = System.currentTimeMillis()
val dt = Math.min(17, newTime - lastReplyButtonAnimationTime)
lastReplyButtonAnimationTime = newTime
val showing = translationX >= minShowDistance
if (showing) {
if (replyButtonProgress < 1.0f) {
replyButtonProgress += dt / 180.0f
if (replyButtonProgress > 1.0f) {
replyButtonProgress = 1.0f
} else {
itemView.invalidate()
}
}
} else if (translationX <= 0.0f) {
replyButtonProgress = 0f
startTracking = false
isVibrate = false
} else {
if (replyButtonProgress > 0.0f) {
replyButtonProgress -= dt / 180.0f
if (replyButtonProgress < 0.1f) {
replyButtonProgress = 0f
} else {
itemView.invalidate()
}
}
}
val alpha: Int
val scale: Float
if (showing) {
scale = if (replyButtonProgress <= 0.8f) {
1.2f * (replyButtonProgress / 0.8f)
} else {
1.2f - 0.2f * ((replyButtonProgress - 0.8f) / 0.2f)
}
alpha = Math.min(255f, 255 * (replyButtonProgress / 0.8f)).toInt()
} else {
scale = replyButtonProgress
alpha = Math.min(255f, 255 * replyButtonProgress).toInt()
}

imageDrawable.alpha = alpha
if (startTracking) {
if (!isVibrate && translationX >= triggerDistance) {
itemView.performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS
// , HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
)
isVibrate = true
}
}

val x: Int = itemView.width - if (translationX > triggerDistance + triggerDelta) {
(convertToPx(130) / 2).toInt()
} else {
(translationX / 2).toInt()
}

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

private fun convertToPx(dp: Int): Float {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
context.resources.displayMetrics
)
}

}

View File

@ -127,26 +127,28 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
//TODO is downloading attachement? //TODO is downloading attachement?


if (!event.root.isRedacted()) { if (!event.root.isRedacted()) {
if (event.canReact()) {
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, eventId))
}
if (canCopy(type)) {
//TODO copy images? html? see ClipBoard
this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent!!.body))
}


if (canReply(event, messageContent)) { if (canReply(event, messageContent)) {
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId)) this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId))
} }


if (canEdit(event, session.sessionParams.credentials.userId)) { if (canEdit(event, session.myUserId)) {
this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, eventId)) this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, eventId))
} }


if (canRedact(event, session.sessionParams.credentials.userId)) { if (canRedact(event, session.myUserId)) {
this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, eventId)) this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, eventId))
} }


if (canCopy(type)) {
//TODO copy images? html? see ClipBoard
this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent!!.body))
}

if (event.canReact()) {
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, eventId))
}

if (canQuote(event, messageContent)) { if (canQuote(event, messageContent)) {
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId)) this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId))
} }
@ -183,7 +185,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
} }
this.add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, event.root.eventId)) this.add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, event.root.eventId))


if (session.sessionParams.credentials.userId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) { if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
//not sent by me //not sent by me
this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, event.root.eventId)) this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, event.root.eventId))
} }

View File

@ -26,6 +26,7 @@ import com.airbnb.mvrx.Success
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.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericFooterItem
@ -60,13 +61,13 @@ class ViewEditHistoryEpoxyController(private val context: Context,
} }
} }
is Success -> { is Success -> {
state.editList()?.let { renderEvents(it) } state.editList()?.let { renderEvents(it, state.isOriginalAReply) }
} }


} }
} }


private fun renderEvents(sourceEvents: List<Event>) { private fun renderEvents(sourceEvents: List<Event>, isOriginalReply: Boolean) {
if (sourceEvents.isEmpty()) { if (sourceEvents.isEmpty()) {
genericItem { genericItem {
id("footer") id("footer")
@ -92,7 +93,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
} }
} }
lastDate = evDate lastDate = evDate
val cContent = getCorrectContent(timelineEvent) val cContent = getCorrectContent(timelineEvent, isOriginalReply)
val body = cContent.second?.let { eventHtmlRenderer.render(it) } val body = cContent.second?.let { eventHtmlRenderer.render(it) }
?: cContent.first ?: cContent.first


@ -101,7 +102,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
var spannedDiff: Spannable? = null var spannedDiff: Spannable? = null
if (nextEvent != null && cContent.second == null /*No diff for html*/) { if (nextEvent != null && cContent.second == null /*No diff for html*/) {
//compares the body //compares the body
val nContent = getCorrectContent(nextEvent) val nContent = getCorrectContent(nextEvent, isOriginalReply)
val nextBody = nContent.second?.let { eventHtmlRenderer.render(it) } val nextBody = nContent.second?.let { eventHtmlRenderer.render(it) }
?: nContent.first ?: nContent.first
val dmp = diff_match_patch() val dmp = diff_match_patch()
@ -144,11 +145,14 @@ class ViewEditHistoryEpoxyController(private val context: Context,
} }
} }


private fun getCorrectContent(event: Event): Pair<String, String?> { private fun getCorrectContent(event: Event, isOriginalReply: Boolean): Pair<String, String?> {
val clearContent = event.getClearContent().toModel<MessageTextContent>() val clearContent = event.getClearContent().toModel<MessageTextContent>()
val newContent = clearContent val newContent = clearContent
?.newContent ?.newContent
?.toModel<MessageTextContent>() ?.toModel<MessageTextContent>()
if (isOriginalReply) {
return extractUsefulTextFromReply(newContent?.body ?: clearContent?.body ?: "") to null
}
return (newContent?.body ?: clearContent?.body ?: "") to (newContent?.formattedBody return (newContent?.body ?: clearContent?.body ?: "") to (newContent?.formattedBody
?: clearContent?.formattedBody) ?: clearContent?.formattedBody)
} }

View File

@ -21,6 +21,9 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
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.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter


@ -28,6 +31,7 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFor
data class ViewEditHistoryViewState( data class ViewEditHistoryViewState(
val eventId: String, val eventId: String,
val roomId: String, val roomId: String,
val isOriginalAReply: Boolean = false,
val editList: Async<List<Event>> = Uninitialized) val editList: Async<List<Event>> = Uninitialized)
: MvRxState { : MvRxState {


@ -77,11 +81,16 @@ class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
override fun onSuccess(data: List<Event>) { override fun onSuccess(data: List<Event>) {
//TODO until supported by API Add original event manually //TODO until supported by API Add original event manually
val withOriginal = data.toMutableList() val withOriginal = data.toMutableList()
var originalIsReply = false
room.getTimeLineEvent(eventId)?.let { room.getTimeLineEvent(eventId)?.let {
withOriginal.add(it.root) withOriginal.add(it.root)
originalIsReply = it.root.getClearContent().toModel<MessageContent>().isReply()
} }
setState { setState {
copy(editList = Success(withOriginal)) copy(
editList = Success(withOriginal),
isOriginalAReply = originalIsReply
)
} }
} }
}) })

View File

@ -16,6 +16,7 @@


package im.vector.riotx.features.home.room.detail.timeline.item package im.vector.riotx.features.home.room.detail.timeline.item


import android.view.MotionEvent
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import androidx.core.text.PrecomputedTextCompat import androidx.core.text.PrecomputedTextCompat
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
@ -40,14 +41,23 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var urlClickCallback: TimelineEventController.UrlClickCallback? = null var urlClickCallback: TimelineEventController.UrlClickCallback? = null


// Better link movement methods fixes the issue when
// long pressing to open the context menu on a TextView also triggers an autoLink click.
private val mvmtMethod = BetterLinkMovementMethod.newInstance().also { private val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
it.setOnLinkClickListener { _, url -> it.setOnLinkClickListener { _, url ->
//Return false to let android manage the click on the link, or true if the link is handled by the application //Return false to let android manage the click on the link, or true if the link is handled by the application
urlClickCallback?.onUrlClicked(url) == true urlClickCallback?.onUrlClicked(url) == true
} }
it.setOnLinkLongClickListener { _, url -> //We need also to fix the case when long click on link will trigger long click on cell
it.setOnLinkLongClickListener { tv, url ->
//Long clicks are handled by parent, return true to block android to do something with url //Long clicks are handled by parent, return true to block android to do something with url
urlClickCallback?.onUrlLongClicked(url) == true if (urlClickCallback?.onUrlLongClicked(url) == true) {
tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0))
true
} else {
false
}

} }
} }



View File

@ -30,10 +30,13 @@ abstract class FilteredRoomFooterItem : VectorEpoxyModel<FilteredRoomFooterItem.
@EpoxyAttribute @EpoxyAttribute
var listener: FilteredRoomFooterItemListener? = null var listener: FilteredRoomFooterItemListener? = null


@EpoxyAttribute
var currentFilter: String = ""

override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.createRoomButton.setOnClickListener { listener?.createRoom() } holder.createRoomButton.setOnClickListener { listener?.createRoom(currentFilter) }
holder.createDirectChat.setOnClickListener { listener?.createDirectChat() } holder.createDirectChat.setOnClickListener { listener?.createDirectChat() }
holder.openRoomDirectory.setOnClickListener { listener?.openRoomDirectory() } holder.openRoomDirectory.setOnClickListener { listener?.openRoomDirectory(currentFilter) }
} }


class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
@ -43,7 +46,7 @@ abstract class FilteredRoomFooterItem : VectorEpoxyModel<FilteredRoomFooterItem.
} }


interface FilteredRoomFooterItemListener : FabMenuView.Listener { interface FilteredRoomFooterItemListener : FabMenuView.Listener {
fun createRoom() fun createRoom(initialName: String)
} }
} }



View File

@ -20,17 +20,15 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.extensions.replaceFragment
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.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.home.room.list.RoomListParams import im.vector.riotx.features.home.room.list.RoomListParams
import kotlinx.android.synthetic.main.activity_filtered_rooms.* import kotlinx.android.synthetic.main.activity_filtered_rooms.*


class FilteredRoomsActivity : VectorBaseActivity(), ToolbarConfigurable { class FilteredRoomsActivity : VectorBaseActivity() {


private lateinit var roomListFragment: RoomListFragment private lateinit var roomListFragment: RoomListFragment


@ -44,6 +42,9 @@ class FilteredRoomsActivity : VectorBaseActivity(), ToolbarConfigurable {


override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

configureToolbar(filteredRoomsToolbar)

if (isFirstCreation()) { if (isFirstCreation()) {
roomListFragment = RoomListFragment.newInstance(RoomListParams(RoomListFragment.DisplayMode.FILTERED)) roomListFragment = RoomListFragment.newInstance(RoomListParams(RoomListFragment.DisplayMode.FILTERED))
replaceFragment(roomListFragment, R.id.filteredRoomsFragmentContainer, FRAGMENT_TAG) replaceFragment(roomListFragment, R.id.filteredRoomsFragmentContainer, FRAGMENT_TAG)
@ -67,10 +68,6 @@ class FilteredRoomsActivity : VectorBaseActivity(), ToolbarConfigurable {
filteredRoomsSearchView.requestFocus() filteredRoomsSearchView.requestFocus()
} }


override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}

companion object { companion object {
private const val FRAGMENT_TAG = "RoomListFragment" private const val FRAGMENT_TAG = "RoomListFragment"



View File

@ -38,6 +38,7 @@ import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.room.list.widget.FabMenuView import im.vector.riotx.features.home.room.list.widget.FabMenuView
import im.vector.riotx.features.notifications.NotificationDrawerManager
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_list.* import kotlinx.android.synthetic.main.fragment_room_list.*
import javax.inject.Inject import javax.inject.Inject
@ -69,6 +70,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
@Inject lateinit var roomController: RoomSummaryController @Inject lateinit var roomController: RoomSummaryController
@Inject lateinit var roomListViewModelFactory: RoomListViewModel.Factory @Inject lateinit var roomListViewModelFactory: RoomListViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
private val roomListViewModel: RoomListViewModel by fragmentViewModel() private val roomListViewModel: RoomListViewModel by fragmentViewModel()


override fun getLayoutResId() = R.layout.fragment_room_list override fun getLayoutResId() = R.layout.fragment_room_list
@ -112,7 +114,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
} }


// Hide FAB when list is scrolling // Hide FAB when list is scrolling
epoxyRecyclerView.addOnScrollListener( roomListEpoxyRecyclerView.addOnScrollListener(
object : RecyclerView.OnScrollListener() { object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
createChatFabMenu.removeCallbacks(showFabRunnable) createChatFabMenu.removeCallbacks(showFabRunnable)
@ -136,11 +138,14 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
} }


fun filterRoomsWith(filter: String) { fun filterRoomsWith(filter: String) {
// Scroll the list to top
roomListEpoxyRecyclerView.scrollToPosition(0)

roomListViewModel.accept(RoomListActions.FilterWith(filter)) roomListViewModel.accept(RoomListActions.FilterWith(filter))
} }


override fun openRoomDirectory() { override fun openRoomDirectory(initialFilter: String) {
navigator.openRoomDirectory(requireActivity()) navigator.openRoomDirectory(requireActivity(), initialFilter)
} }


override fun createDirectChat() { override fun createDirectChat() {
@ -150,12 +155,12 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
private fun setupRecyclerView() { private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context) val layoutManager = LinearLayoutManager(context)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
epoxyRecyclerView.layoutManager = layoutManager roomListEpoxyRecyclerView.layoutManager = layoutManager
epoxyRecyclerView.itemAnimator = RoomListAnimator() roomListEpoxyRecyclerView.itemAnimator = RoomListAnimator()
roomController.listener = this roomController.listener = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) } roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
stateView.contentView = epoxyRecyclerView stateView.contentView = roomListEpoxyRecyclerView
epoxyRecyclerView.setController(roomController) roomListEpoxyRecyclerView.setController(roomController)
} }


private val showFabRunnable = Runnable { private val showFabRunnable = Runnable {
@ -255,10 +260,12 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
} }


override fun onAcceptRoomInvitation(room: RoomSummary) { override fun onAcceptRoomInvitation(room: RoomSummary) {
notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId)
roomListViewModel.accept(RoomListActions.AcceptInvitation(room)) roomListViewModel.accept(RoomListActions.AcceptInvitation(room))
} }


override fun onRejectRoomInvitation(room: RoomSummary) { override fun onRejectRoomInvitation(room: RoomSummary) {
notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId)
roomListViewModel.accept(RoomListActions.RejectInvitation(room)) roomListViewModel.accept(RoomListActions.RejectInvitation(room))
} }


@ -266,9 +273,8 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory)) roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory))
} }


// TODO Pass title override fun createRoom(initialName: String) {
override fun createRoom() { navigator.openCreateRoom(requireActivity(), initialName)
navigator.openCreateRoom(requireActivity())
} }


} }

View File

@ -70,13 +70,14 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
viewState.rejectingRoomsIds, viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds) viewState.rejectingErrorRoomsIds)


addFilterFooter() addFilterFooter(viewState)
} }


private fun addFilterFooter() { private fun addFilterFooter(viewState: RoomListViewState) {
filteredRoomFooterItem { filteredRoomFooterItem {
id("filter_footer") id("filter_footer")
listener(listener) listener(listener)
currentFilter(viewState.roomFilter)
} }
} }



View File

@ -92,7 +92,7 @@ class FabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSe


interface Listener { interface Listener {
fun createDirectChat() fun createDirectChat()
fun openRoomDirectory() fun openRoomDirectory(initialFilter: String = "")
} }


} }

View File

@ -22,7 +22,7 @@ import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible import androidx.core.view.isVisible
import arrow.core.Try import arrow.core.Try
import com.jakewharton.rxbinding2.widget.RxTextView import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
@ -127,9 +127,9 @@ class LoginActivity : VectorBaseActivity() {
private fun setupAuthButton() { private fun setupAuthButton() {
Observable Observable
.combineLatest( .combineLatest(
RxTextView.textChanges(loginField).map { it.trim().isNotEmpty() }, loginField.textChanges().map { it.trim().isNotEmpty() },
RxTextView.textChanges(passwordField).map { it.trim().isNotEmpty() }, passwordField.textChanges().map { it.trim().isNotEmpty() },
RxTextView.textChanges(homeServerField).map { it.trim().isNotEmpty() }, homeServerField.textChanges().map { it.trim().isNotEmpty() },
Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty -> Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty ->
isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty
} }

View File

@ -60,13 +60,13 @@ class DefaultNavigator @Inject constructor() : Navigator {
context.startActivity(intent) context.startActivity(intent)
} }


override fun openRoomDirectory(context: Context) { override fun openRoomDirectory(context: Context, initialFilter: String) {
val intent = Intent(context, RoomDirectoryActivity::class.java) val intent = RoomDirectoryActivity.getIntent(context, initialFilter)
context.startActivity(intent) context.startActivity(intent)
} }


override fun openCreateRoom(context: Context) { override fun openCreateRoom(context: Context, initialName: String) {
val intent = CreateRoomActivity.getIntent(context) val intent = CreateRoomActivity.getIntent(context, initialName)
context.startActivity(intent) context.startActivity(intent)
} }



View File

@ -27,11 +27,11 @@ interface Navigator {


fun openRoomPreview(publicRoom: PublicRoom, context: Context) fun openRoomPreview(publicRoom: PublicRoom, context: Context)


fun openCreateRoom(context: Context) fun openCreateRoom(context: Context, initialName: String = "")


fun openCreateDirectRoom(context: Context) fun openCreateDirectRoom(context: Context)


fun openRoomDirectory(context: Context) fun openRoomDirectory(context: Context, initialFilter: String = "")


fun openRoomsFiltering(context: Context) fun openRoomsFiltering(context: Context)



View File

@ -70,7 +70,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
val bodyPreview = event.type val bodyPreview = event.type


return SimpleNotifiableEvent( return SimpleNotifiableEvent(
session.sessionParams.credentials.userId, session.myUserId,
eventId = event.eventId!!, eventId = event.eventId!!,
noisy = false,//will be updated noisy = false,//will be updated
timestamp = event.originServerTs ?: System.currentTimeMillis(), timestamp = event.originServerTs ?: System.currentTimeMillis(),
@ -109,7 +109,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
roomId = event.root.roomId!!, roomId = event.root.roomId!!,
roomName = roomName) roomName = roomName)


notifiableEvent.matrixID = session.sessionParams.credentials.userId notifiableEvent.matrixID = session.myUserId
return notifiableEvent return notifiableEvent
} else { } else {
if (event.root.isEncrypted() && event.root.mxDecryptionResult == null) { if (event.root.isEncrypted() && event.root.mxDecryptionResult == null) {
@ -145,7 +145,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
roomName = roomName, roomName = roomName,
roomIsDirect = room.roomSummary()?.isDirect ?: false) roomIsDirect = room.roomSummary()?.isDirect ?: false)


notifiableEvent.matrixID = session.sessionParams.credentials.userId notifiableEvent.matrixID = session.myUserId
notifiableEvent.soundName = null notifiableEvent.soundName = null


// Get the avatars URL // Get the avatars URL
@ -175,7 +175,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
val body = noticeEventFormatter.format(event, dName) val body = noticeEventFormatter.format(event, dName)
?: stringProvider.getString(R.string.notification_new_invitation) ?: stringProvider.getString(R.string.notification_new_invitation)
return InviteNotifiableEvent( return InviteNotifiableEvent(
session.sessionParams.credentials.userId, session.myUserId,
eventId = event.eventId!!, eventId = event.eventId!!,
roomId = roomId, roomId = roomId,
timestamp = event.originServerTs ?: 0, timestamp = event.originServerTs ?: 0,

View File

@ -121,9 +121,9 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
false, false,
System.currentTimeMillis(), System.currentTimeMillis(),
session.getUser(session.sessionParams.credentials.userId)?.displayName session.getUser(session.myUserId)?.displayName
?: context?.getString(R.string.notification_sender_me), ?: context?.getString(R.string.notification_sender_me),
session.sessionParams.credentials.userId, session.myUserId,
message, message,
room.roomId, room.roomId,
room.roomSummary()?.displayName ?: room.roomId, room.roomSummary()?.displayName ?: room.roomId,

View File

@ -181,8 +181,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context


val session = activeSessionHolder.getSafeActiveSession() ?: return val session = activeSessionHolder.getSafeActiveSession() ?: return


val user = session.getUser(session.sessionParams.credentials.userId) val user = session.getUser(session.myUserId)
val myUserDisplayName = user?.displayName ?: session.sessionParams.credentials.userId // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
val myUserDisplayName = user?.displayName?.takeIf { it.isNotBlank() } ?: session.myUserId
val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE) val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE)
synchronized(eventList) { synchronized(eventList) {


@ -343,7 +344,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
for (event in simpleEvents) { for (event in simpleEvents) {
//We build a simple event //We build a simple event
if (firstTime || !event.hasBeenDisplayed) { if (firstTime || !event.hasBeenDisplayed) {
NotificationUtils.buildSimpleEventNotification(context, event, null, myUserDisplayName)?.let { NotificationUtils.buildSimpleEventNotification(context, event, null, session.myUserId)?.let {
notifications.add(it) notifications.add(it)
NotificationUtils.showNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID, it) NotificationUtils.showNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID, it)
event.hasBeenDisplayed = true //we can consider it as displayed event.hasBeenDisplayed = true //we can consider it as displayed

View File

@ -204,7 +204,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
var olmVersion = "undefined" var olmVersion = "undefined"


activeSessionHolder.getSafeActiveSession()?.let { session -> activeSessionHolder.getSafeActiveSession()?.let { session ->
userId = session.sessionParams.credentials.userId userId = session.myUserId
deviceId = session.sessionParams.credentials.deviceId ?: "undefined" deviceId = session.sessionParams.credentials.deviceId ?: "undefined"
olmVersion = session.getCryptoVersion(context, true) olmVersion = session.getCryptoVersion(context, true)
} }

View File

@ -25,14 +25,13 @@ import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding2.widget.RxTextView import com.jakewharton.rxbinding3.appcompat.queryTextChanges
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.themes.ThemeUtils
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.fragment_public_rooms.* import kotlinx.android.synthetic.main.fragment_public_rooms.*
import timber.log.Timber import timber.log.Timber
@ -70,9 +69,7 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
it.setDisplayHomeAsUpEnabled(true) it.setDisplayHomeAsUpEnabled(true)
} }


publicRoomsFilter.setBackgroundResource(ThemeUtils.getResourceId(requireContext(), R.drawable.bg_search_edit_text_light)) publicRoomsFilter.queryTextChanges()

RxTextView.textChanges(publicRoomsFilter)
.debounce(500, TimeUnit.MILLISECONDS) .debounce(500, TimeUnit.MILLISECONDS)
.subscribeBy { .subscribeBy {
viewModel.filterWith(it.toString()) viewModel.filterWith(it.toString())
@ -147,6 +144,11 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
} }


override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
if (publicRoomsFilter.query.toString() != state.currentFilter) {
// For initial filter
publicRoomsFilter.setQuery(state.currentFilter, false)
}

// Populate list with Epoxy // Populate list with Epoxy
publicRoomsController.setData(state) publicRoomsController.setData(state)
} }

View File

@ -22,6 +22,8 @@ import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom


data class PublicRoomsViewState( data class PublicRoomsViewState(
// The current filter
val currentFilter: String = "",
// Store cumul of pagination result // Store cumul of pagination result
val publicRooms: List<PublicRoom> = emptyList(), val publicRooms: List<PublicRoom> = emptyList(),
// Current pagination request // Current pagination request

View File

@ -16,8 +16,11 @@


package im.vector.riotx.features.roomdirectory package im.vector.riotx.features.roomdirectory


import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.viewModel
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragment
@ -25,6 +28,7 @@ import im.vector.riotx.core.extensions.addFragmentToBackstack
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomViewModel
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
import javax.inject.Inject import javax.inject.Inject


@ -39,7 +43,10 @@ class RoomDirectoryActivity : VectorBaseActivity() {
} }




@Inject lateinit var createRoomViewModelFactory: CreateRoomViewModel.Factory
@Inject lateinit var roomDirectoryViewModelFactory: RoomDirectoryViewModel.Factory @Inject lateinit var roomDirectoryViewModelFactory: RoomDirectoryViewModel.Factory
private val roomDirectoryViewModel: RoomDirectoryViewModel by viewModel()
private val createRoomViewModel: CreateRoomViewModel by viewModel()
private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel


override fun getLayoutRes() = R.layout.activity_simple override fun getLayoutRes() = R.layout.activity_simple
@ -51,6 +58,11 @@ class RoomDirectoryActivity : VectorBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(RoomDirectoryNavigationViewModel::class.java) navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(RoomDirectoryNavigationViewModel::class.java)

if (isFirstCreation()) {
roomDirectoryViewModel.filterWith(intent?.getStringExtra(INITIAL_FILTER) ?: "")
}

navigationViewModel.navigateTo.observeEvent(this) { navigation -> navigationViewModel.navigateTo.observeEvent(this) { navigation ->
when (navigation) { when (navigation) {
is Navigation.Back -> onBackPressed() is Navigation.Back -> onBackPressed()
@ -59,6 +71,11 @@ class RoomDirectoryActivity : VectorBaseActivity() {
is Navigation.Close -> finish() is Navigation.Close -> finish()
} }
} }

roomDirectoryViewModel.selectSubscribe(this, PublicRoomsViewState::currentFilter) { currentFilter ->
// Transmit the filter to the createRoomViewModel
createRoomViewModel.setName(currentFilter)
}
} }


override fun initUiAndData() { override fun initUiAndData() {
@ -67,4 +84,13 @@ class RoomDirectoryActivity : VectorBaseActivity() {
} }
} }


companion object {
private const val INITIAL_FILTER = "INITIAL_FILTER"

fun getIntent(context: Context, initialFilter: String = ""): Intent {
val intent = Intent(context, RoomDirectoryActivity::class.java)
intent.putExtra(INITIAL_FILTER, initialFilter)
return intent
}
}
} }

View File

@ -59,9 +59,6 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
get() = _joinRoomErrorLiveData get() = _joinRoomErrorLiveData




// TODO Store in ViewState?
private var currentFilter: String = ""

private var since: String? = null private var since: String? = null


private var currentTask: Cancelable? = null private var currentTask: Cancelable? = null
@ -70,9 +67,6 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
private var roomDirectoryData = RoomDirectoryData() private var roomDirectoryData = RoomDirectoryData()


init { init {
// Load with empty filter
load()

setState { setState {
copy( copy(
roomDirectoryDisplayName = roomDirectoryData.displayName roomDirectoryDisplayName = roomDirectoryData.displayName
@ -115,24 +109,20 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:


this.roomDirectoryData = roomDirectoryData this.roomDirectoryData = roomDirectoryData


reset() reset("")
load() load("")
}

fun filterWith(filter: String) {
if (currentFilter == filter) {
return
} }


fun filterWith(filter: String) = withState { state ->
if (state.currentFilter != filter) {
currentTask?.cancel() currentTask?.cancel()


currentFilter = filter reset(filter)

load(filter)
reset() }
load()
} }


private fun reset() { private fun reset(newFilter: String) {
// Reset since token // Reset since token
since = null since = null


@ -141,12 +131,13 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
publicRooms = emptyList(), publicRooms = emptyList(),
asyncPublicRoomsRequest = Loading(), asyncPublicRoomsRequest = Loading(),
hasMore = false, hasMore = false,
roomDirectoryDisplayName = roomDirectoryData.displayName roomDirectoryDisplayName = roomDirectoryData.displayName,
currentFilter = newFilter
) )
} }
} }


fun loadMore() { fun loadMore() = withState { state ->
if (currentTask == null) { if (currentTask == null) {
setState { setState {
copy( copy(
@ -154,15 +145,15 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
) )
} }


load() load(state.currentFilter)
} }
} }


private fun load() { private fun load(filter: String) {
currentTask = session.getPublicRooms(roomDirectoryData.homeServer, currentTask = session.getPublicRooms(roomDirectoryData.homeServer,
PublicRoomsParams( PublicRoomsParams(
limit = PUBLIC_ROOMS_LIMIT, limit = PUBLIC_ROOMS_LIMIT,
filter = PublicRoomsFilter(searchTerm = currentFilter), filter = PublicRoomsFilter(searchTerm = filter),
includeAllNetworks = roomDirectoryData.includeAllNetworks, includeAllNetworks = roomDirectoryData.includeAllNetworks,
since = since, since = since,
thirdPartyInstanceId = roomDirectoryData.thirdPartyInstanceId thirdPartyInstanceId = roomDirectoryData.thirdPartyInstanceId

View File

@ -21,6 +21,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.viewModel
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragment
@ -29,12 +30,16 @@ 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.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.RoomDirectoryNavigationViewModel import im.vector.riotx.features.roomdirectory.RoomDirectoryNavigationViewModel
import javax.inject.Inject


/** /**
* Simple container for [CreateRoomFragment] * Simple container for [CreateRoomFragment]
*/ */
class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable { class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable {


@Inject lateinit var createRoomViewModelFactory: CreateRoomViewModel.Factory
private val createRoomViewModel: CreateRoomViewModel by viewModel()

private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel


override fun getLayoutRes() = R.layout.activity_simple override fun getLayoutRes() = R.layout.activity_simple
@ -46,6 +51,8 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable {
override fun initUiAndData() { override fun initUiAndData() {
if (isFirstCreation()) { if (isFirstCreation()) {
addFragment(CreateRoomFragment(), R.id.simpleFragmentContainer) addFragment(CreateRoomFragment(), R.id.simpleFragmentContainer)

createRoomViewModel.setName(intent?.getStringExtra(INITIAL_NAME) ?: "")
} }
} }


@ -58,14 +65,19 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable {
navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(RoomDirectoryNavigationViewModel::class.java) navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(RoomDirectoryNavigationViewModel::class.java)
navigationViewModel.navigateTo.observeEvent(this) { navigation -> navigationViewModel.navigateTo.observeEvent(this) { navigation ->
when (navigation) { when (navigation) {
is RoomDirectoryActivity.Navigation.Back -> finish() is RoomDirectoryActivity.Navigation.Back,
is RoomDirectoryActivity.Navigation.Close -> finish()
} }
} }
} }


companion object { companion object {
fun getIntent(context: Context): Intent { private const val INITIAL_NAME = "INITIAL_NAME"
return Intent(context, CreateRoomActivity::class.java)
fun getIntent(context: Context, initialName: String = ""): Intent {
return Intent(context, CreateRoomActivity::class.java).apply {
putExtra(INITIAL_NAME, initialName)
}
} }
} }



View File

@ -21,7 +21,7 @@ import android.view.MenuItem
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
@ -35,9 +35,8 @@ import javax.inject.Inject
class CreateRoomFragment : VectorBaseFragment(), CreateRoomController.Listener { class CreateRoomFragment : VectorBaseFragment(), CreateRoomController.Listener {


private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel
private val viewModel: CreateRoomViewModel by fragmentViewModel() private val viewModel: CreateRoomViewModel by activityViewModel()
@Inject lateinit var createRoomController: CreateRoomController @Inject lateinit var createRoomController: CreateRoomController
@Inject lateinit var createRoomViewModelFactory: CreateRoomViewModel.Factory


override fun getLayoutResId() = R.layout.fragment_create_room override fun getLayoutResId() = R.layout.fragment_create_room



View File

@ -16,6 +16,7 @@


package im.vector.riotx.features.roomdirectory.createroom package im.vector.riotx.features.roomdirectory.createroom


import androidx.fragment.app.FragmentActivity
import com.airbnb.mvrx.* import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
@ -25,6 +26,7 @@ import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity


class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateRoomViewState, class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateRoomViewState,
private val session: Session private val session: Session
@ -39,8 +41,13 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr


@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: CreateRoomViewState): CreateRoomViewModel? { override fun create(viewModelContext: ViewModelContext, state: CreateRoomViewState): CreateRoomViewModel? {
val fragment: CreateRoomFragment = (viewModelContext as FragmentViewModelContext).fragment() val activity: FragmentActivity = (viewModelContext as ActivityViewModelContext).activity()
return fragment.createRoomViewModelFactory.create(state)
return when (activity) {
is CreateRoomActivity -> activity.createRoomViewModelFactory.create(state)
is RoomDirectoryActivity -> activity.createRoomViewModelFactory.create(state)
else -> throw IllegalStateException("Wrong activity")
}
} }
} }



View File

@ -148,6 +148,7 @@ object VectorPreferences {
private const val SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY = "SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY" private const val SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY = "SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY"


private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"


// analytics // analytics
const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY" const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY"
@ -249,6 +250,10 @@ object VectorPreferences {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false) return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false)
} }


fun swipeToReplyIsEnabled(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY, true)
}

/** /**
* Tells if we have already asked the user to disable battery optimisations on android >= M devices. * Tells if we have already asked the user to disable battery optimisations on android >= M devices.
* *

View File

@ -79,9 +79,9 @@ class VectorSettingsActivity : VectorBaseActivity(),
var oFragment: Fragment? = null var oFragment: Fragment? = null


if (VectorPreferences.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref?.key) { if (VectorPreferences.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref?.key) {
oFragment = VectorSettingsNotificationsTroubleshootFragment.newInstance(session.sessionParams.credentials.userId) oFragment = VectorSettingsNotificationsTroubleshootFragment.newInstance(session.myUserId)
} else if (VectorPreferences.SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY == pref?.key) { } else if (VectorPreferences.SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY == pref?.key) {
oFragment = VectorSettingsAdvancedNotificationPreferenceFragment.newInstance(session.sessionParams.credentials.userId) oFragment = VectorSettingsAdvancedNotificationPreferenceFragment.newInstance(session.myUserId)
} else { } else {
try { try {
pref?.fragment?.let { pref?.fragment?.let {

View File

@ -95,7 +95,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {


// Display name // Display name
mDisplayNamePreference.let { mDisplayNamePreference.let {
it.summary = session.getUser(session.sessionParams.credentials.userId)?.displayName ?: "" it.summary = session.getUser(session.myUserId)?.displayName ?: ""
it.text = it.summary.toString() it.text = it.summary.toString()
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
onDisplayNameClick(newValue?.let { (it as String).trim() }) onDisplayNameClick(newValue?.let { (it as String).trim() })
@ -148,7 +148,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {


// user account // user account
findPreference(VectorPreferences.SETTINGS_LOGGED_IN_PREFERENCE_KEY) findPreference(VectorPreferences.SETTINGS_LOGGED_IN_PREFERENCE_KEY)
.summary = session.sessionParams.credentials.userId .summary = session.myUserId


// home server // home server
findPreference(VectorPreferences.SETTINGS_HOME_SERVER_PREFERENCE_KEY) findPreference(VectorPreferences.SETTINGS_HOME_SERVER_PREFERENCE_KEY)

View File

@ -367,7 +367,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() {
* @param aMyDeviceInfo the device info * @param aMyDeviceInfo the device info
*/ */
private fun refreshCryptographyPreference(aMyDeviceInfo: DeviceInfo?) { private fun refreshCryptographyPreference(aMyDeviceInfo: DeviceInfo?) {
val userId = session.sessionParams.credentials.userId val userId = session.myUserId
val deviceId = session.sessionParams.credentials.deviceId val deviceId = session.sessionParams.credentials.deviceId


// device name // device name

View File

@ -35,7 +35,7 @@ class SignOutUiWorker(private val activity: FragmentActivity) {
activeSessionHolder = context.vectorComponent().activeSessionHolder() activeSessionHolder = context.vectorComponent().activeSessionHolder()
val session = activeSessionHolder.getActiveSession() val session = activeSessionHolder.getActiveSession()
if (SignOutViewModel.doYouNeedToBeDisplayed(session)) { if (SignOutViewModel.doYouNeedToBeDisplayed(session)) {
val signOutDialog = SignOutBottomSheetDialogFragment.newInstance(session.sessionParams.credentials.userId) val signOutDialog = SignOutBottomSheetDialogFragment.newInstance(session.myUserId)
signOutDialog.onSignOut = Runnable { signOutDialog.onSignOut = Runnable {
doSignOut() doSignOut()
} }

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M9,9m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#03B381"
android:strokeLineCap="round"/>
<path
android:pathData="M19,19l-4.35,-4.35"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#03B381"
android:strokeLineCap="round"/>
</vector>

View File

@ -14,16 +14,16 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:elevation="4dp" android:elevation="4dp"
app:contentInsetStart="0dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">


<androidx.appcompat.widget.SearchView <androidx.appcompat.widget.SearchView
android:id="@+id/filteredRoomsSearchView" android:id="@+id/filteredRoomsSearchView"
style="@style/VectorSearchView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:closeIcon="@drawable/ic_x_green"
app:iconifiedByDefault="false"
app:queryHint="@string/room_filtering_filter_hint" app:queryHint="@string/room_filtering_filter_hint"
app:searchIcon="@drawable/ic_filter" /> app:searchIcon="@drawable/ic_filter" />



View File

@ -7,18 +7,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="8dp"> android:padding="8dp">


<TextView
android:id="@+id/quickReactionTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="@string/quick_reactions"
android:textColor="?riotx_text_secondary"
android:textSize="16sp"
app:layout_constraintTop_toTopOf="parent" />


<TextView <TextView
android:id="@+id/quickReaction0" android:id="@+id/quickReaction0"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -104,16 +92,14 @@
android:id="@+id/reactionsFlowHelper" android:id="@+id/reactionsFlowHelper"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
app:constraint_referenced_ids="quickReaction0,quickReaction1,quickReaction2,quickReaction3,quickReaction4,quickReaction5,quickReaction6,quickReaction7" app:constraint_referenced_ids="quickReaction0,quickReaction1,quickReaction2,quickReaction3,quickReaction4,quickReaction5,quickReaction6,quickReaction7"
app:flow_horizontalGap="8dp" app:flow_horizontalGap="0dp"
app:flow_horizontalStyle="spread" app:flow_horizontalStyle="spread"
app:flow_verticalBias="0" app:flow_verticalBias="0"
app:flow_verticalGap="4dp" app:flow_verticalGap="4dp"
app:flow_wrapMode="chain" app:flow_wrapMode="chain"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/quickReactionTitle" /> app:layout_constraintTop_toTopOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -28,6 +28,7 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />


<TextView <TextView

View File

@ -11,10 +11,10 @@


<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/createRoomToolbar" android:id="@+id/createRoomToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="?actionBarSize" android:layout_height="?actionBarSize"
android:elevation="4dp" android:elevation="4dp"
app:contentInsetStartWithNavigation="0dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">

View File

@ -8,10 +8,10 @@


<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/groupToolbar" android:id="@+id/groupToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:elevation="4dp" android:elevation="4dp"
app:contentInsetStartWithNavigation="0dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">



View File

@ -23,36 +23,22 @@


<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/publicRoomsToolbar" android:id="@+id/publicRoomsToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:elevation="4dp" android:elevation="4dp"
android:minHeight="0dp" android:minHeight="0dp"
app:contentInsetStartWithNavigation="0dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"> app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways">


<!-- Note: Background is modified in the code for other themes --> <androidx.appcompat.widget.SearchView
<EditText
android:id="@+id/publicRoomsFilter" android:id="@+id/publicRoomsFilter"
style="@style/VectorSearchView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="32dp" android:layout_height="wrap_content"
android:layout_marginStart="8dp" app:queryHint="@string/room_directory_search_hint" />
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_search_edit_text_light"
android:drawableStart="@drawable/ic_search_white"
android:drawableLeft="@drawable/ic_search_white"
android:drawablePadding="8dp"
android:drawableTint="?riotx_text_secondary"
android:hint="@string/home_filter_placeholder_rooms"
android:lines="1"
android:paddingLeft="8dp"
android:paddingRight="8dp" />


</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>



View File

@ -8,10 +8,10 @@


<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/roomToolbar" android:id="@+id/roomToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="?actionBarSize" android:layout_height="?actionBarSize"
android:elevation="4dp" android:elevation="4dp"
app:contentInsetStartWithNavigation="0dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">

View File

@ -8,7 +8,7 @@
android:background="?riotx_header_panel_background"> android:background="?riotx_header_panel_background">


<com.airbnb.epoxy.EpoxyRecyclerView <com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/epoxyRecyclerView" android:id="@+id/roomListEpoxyRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />



View File

@ -13,10 +13,10 @@


<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/roomPreviewNoPreviewToolbar" android:id="@+id/roomPreviewNoPreviewToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?actionBarSize" android:layout_height="?actionBarSize"
android:elevation="4dp" android:elevation="4dp">
app:contentInsetStartWithNavigation="0dp">


<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1534,7 +1534,6 @@ Why choose Riot.im?
<string name="settings_other_third_party_notices">Other third party notices</string> <string name="settings_other_third_party_notices">Other third party notices</string>
<string name="navigate_to_room_when_already_in_the_room">You are already viewing this room!</string> <string name="navigate_to_room_when_already_in_the_room">You are already viewing this room!</string>


<string name="quick_reactions">Quick Reactions</string>


<!-- Settings --> <!-- Settings -->
<string name="settings_general_title">General</string> <string name="settings_general_title">General</string>

View File

@ -32,5 +32,11 @@
<string name="room_filtering_footer_create_new_direct_message">Send a new direct message</string> <string name="room_filtering_footer_create_new_direct_message">Send a new direct message</string>
<string name="room_filtering_footer_open_room_directory">View the room directory</string> <string name="room_filtering_footer_open_room_directory">View the room directory</string>


<string name="room_directory_search_hint">Name or ID (#example:matrix.org)</string>


<string name="labs_swipe_to_reply_in_timeline">Enable swipe to reply in timeline</string>

<string name="link_copied_to_clipboard">Link copied to clipboard</string>


</resources> </resources>

View File

@ -9,6 +9,7 @@
<item name="titleTextAppearance">@style/Vector.Toolbar.Title</item> <item name="titleTextAppearance">@style/Vector.Toolbar.Title</item>
<item name="subtitleTextAppearance">@style/Vector.Toolbar.SubTitle</item> <item name="subtitleTextAppearance">@style/Vector.Toolbar.SubTitle</item>
<item name="android:background">?riotx_background</item> <item name="android:background">?riotx_background</item>
<item name="contentInsetStartWithNavigation">0dp</item>
</style> </style>


<style name="VectorToolbarStyle.Group"> <style name="VectorToolbarStyle.Group">
@ -160,6 +161,12 @@
<item name="colorControlHighlight">@android:color/white</item> <item name="colorControlHighlight">@android:color/white</item>
</style> </style>


<style name="VectorSearchView" parent="Widget.AppCompat.SearchView">
<item name="searchIcon">@drawable/ic_search</item>
<item name="closeIcon">@drawable/ic_x_green</item>
<item name="iconifiedByDefault">false</item>
</style>

<style name="VectorSearches.EditText" parent="Widget.AppCompat.EditText"> <style name="VectorSearches.EditText" parent="Widget.AppCompat.EditText">
<item name="android:textCursorDrawable">@drawable/searches_cursor_background</item> <item name="android:textCursorDrawable">@drawable/searches_cursor_background</item>
<item name="android:background">@android:color/transparent</item> <item name="android:background">@android:color/transparent</item>

View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">


<!--<im.vector.riotx.core.preference.VectorPreferenceCategory--> <!--<im.vector.riotx.core.preference.VectorPreferenceCategory-->
<!--android:key="SETTINGS_LABS_PREFERENCE_KEY"--> <!--android:key="SETTINGS_LABS_PREFERENCE_KEY"-->
@ -37,10 +35,16 @@
<!--android:title="@string/settings_labs_enable_send_voice" />--> <!--android:title="@string/settings_labs_enable_send_voice" />-->


<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
android:defaultValue="false" android:defaultValue="false"
android:key="SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
android:title="@string/settings_labs_show_hidden_events_in_timeline" /> android:title="@string/settings_labs_show_hidden_events_in_timeline" />



<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true"
android:key="SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
android:title="@string/labs_swipe_to_reply_in_timeline" />

<!--</im.vector.riotx.core.preference.VectorPreferenceCategory>--> <!--</im.vector.riotx.core.preference.VectorPreferenceCategory>-->


</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>