forked from GitHub-Mirror/riotX-android
Merge branch 'develop' into feature/create_direct_room
This commit is contained in:
commit
4341b0d0f5
10
CHANGES.md
10
CHANGES.md
@ -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:
|
||||||
-
|
-
|
||||||
|
15
build.gradle
15
build.gradle
@ -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) {
|
||||||
|
@ -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'
|
||||||
|
@ -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.
|
||||||
|
@ -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"
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
@ -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?
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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 ?: ""
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.api.util
|
||||||
|
|
||||||
|
|
||||||
|
object ContentUtils {
|
||||||
|
fun extractUsefulTextFromReply(repliedBody: String): String {
|
||||||
|
val lines = repliedBody.lines()
|
||||||
|
var wellFormed = repliedBody.startsWith(">")
|
||||||
|
var endOfPreviousFound = false
|
||||||
|
val usefullines = ArrayList<String>()
|
||||||
|
lines.forEach {
|
||||||
|
if (it == "") {
|
||||||
|
endOfPreviousFound = true
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
if (!endOfPreviousFound) {
|
||||||
|
wellFormed = wellFormed && it.startsWith(">")
|
||||||
|
} else {
|
||||||
|
usefullines.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
|
||||||
|
if (repliedBody.startsWith("<mx-reply>")) {
|
||||||
|
val closingTagIndex = repliedBody.lastIndexOf("</mx-reply>")
|
||||||
|
if (closingTagIndex != -1)
|
||||||
|
return repliedBody.substring(closingTagIndex + "</mx-reply>".length).trim()
|
||||||
|
}
|
||||||
|
return repliedBody
|
||||||
|
}
|
||||||
|
}
|
@ -50,16 +50,10 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * Copyright 2019 New Vector Ltd
|
||||||
|
* *
|
||||||
|
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* * you may not use this file except in compliance with the License.
|
||||||
|
* * You may obtain a copy of the License at
|
||||||
|
* *
|
||||||
|
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* *
|
||||||
|
* * Unless required by applicable law or agreed to in writing, software
|
||||||
|
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* * See the License for the specific language governing permissions and
|
||||||
|
* * limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.network
|
||||||
|
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import retrofit2.*
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
|
suspend fun <T> Call<T>.awaitResponse(): Response<T> {
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
enqueue(object : Callback<T> {
|
||||||
|
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||||
|
continuation.resume(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||||
|
continuation.resumeWithException(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -94,19 +94,21 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun requireBackgroundSync() {
|
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 {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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 {
|
||||||
|
@ -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>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.membership
|
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class RoomMemberDisplayNameResolver @Inject constructor() {
|
|
||||||
|
|
||||||
fun resolve(userId: String, members: Map<String, RoomMember>): String? {
|
|
||||||
val currentMember = members[userId]
|
|
||||||
var displayName = currentMember?.displayName
|
|
||||||
// Get the user display name from the member list of the room
|
|
||||||
// Do not consider null display name
|
|
||||||
|
|
||||||
if (currentMember != null && !currentMember.displayName.isNullOrEmpty()) {
|
|
||||||
val hasNameCollision = members
|
|
||||||
.filterValues { it != currentMember && it.displayName == currentMember.displayName }
|
|
||||||
.isNotEmpty()
|
|
||||||
if (hasNameCollision) {
|
|
||||||
displayName = "${currentMember.displayName} ( $userId )"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO handle invited users
|
|
||||||
/*else if (null != member && TextUtils.equals(member!!.membership, RoomMember.MEMBERSHIP_INVITE)) {
|
|
||||||
val user = (mDataHandler as MXDataHandler).getUser(userId)
|
|
||||||
if (null != user) {
|
|
||||||
displayName = user!!.displayname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (displayName == null) {
|
|
||||||
// By default, use the user ID
|
|
||||||
displayName = userId
|
|
||||||
}
|
|
||||||
return displayName
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -33,6 +33,7 @@ import io.realm.Sort
|
|||||||
* This class is an helper around STATE_ROOM_MEMBER events.
|
* 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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.user
|
|
||||||
|
|
||||||
import arrow.core.Try
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.UserEntity
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
|
||||||
import im.vector.matrix.android.internal.task.Task
|
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal interface UpdateUserTask : Task<UpdateUserTask.Params, Unit> {
|
|
||||||
|
|
||||||
data class Params(val eventIds: List<String>)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DefaultUpdateUserTask @Inject constructor(private val monarchy: Monarchy) : UpdateUserTask {
|
|
||||||
|
|
||||||
override suspend fun execute(params: UpdateUserTask.Params): Try<Unit> {
|
|
||||||
return monarchy.tryTransactionSync { realm ->
|
|
||||||
params.eventIds.forEach { eventId ->
|
|
||||||
val event = EventEntity.where(realm, eventId).findFirst()?.asDomain()
|
|
||||||
?: return@forEach
|
|
||||||
val roomId = event.roomId ?: return@forEach
|
|
||||||
val userId = event.stateKey ?: return@forEach
|
|
||||||
val roomMember = RoomMembers(realm, roomId).get(userId) ?: return@forEach
|
|
||||||
if (roomMember.membership != Membership.JOIN) return@forEach
|
|
||||||
|
|
||||||
val userEntity = UserEntity.where(realm, userId).findFirst()
|
|
||||||
?: realm.createObject(UserEntity::class.java, userId)
|
|
||||||
userEntity.displayName = roomMember.displayName ?: ""
|
|
||||||
userEntity.avatarUrl = roomMember.avatarUrl ?: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.user
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
|
import im.vector.matrix.android.internal.database.model.UserEntity
|
||||||
|
|
||||||
|
internal object UserEntityFactory {
|
||||||
|
|
||||||
|
fun createOrNull(event: Event): UserEntity? {
|
||||||
|
if (event.type != EventType.STATE_ROOM_MEMBER) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val roomMember = event.content.toModel<RoomMember>() ?: return null
|
||||||
|
return UserEntity(event.stateKey ?: "",
|
||||||
|
roomMember.displayName ?: "",
|
||||||
|
roomMember.avatarUrl ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.user
|
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
|
||||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
|
||||||
import im.vector.matrix.android.internal.database.query.types
|
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
|
||||||
import im.vector.matrix.android.internal.task.TaskThread
|
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
|
||||||
import io.realm.OrderedCollectionChangeSet
|
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import io.realm.RealmResults
|
|
||||||
import io.realm.Sort
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class UserEntityUpdater @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
|
|
||||||
private val updateUserTask: UpdateUserTask,
|
|
||||||
private val taskExecutor: TaskExecutor)
|
|
||||||
: RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
|
||||||
|
|
||||||
override val query = Monarchy.Query<EventEntity> {
|
|
||||||
EventEntity
|
|
||||||
.types(it, listOf(EventType.STATE_ROOM_MEMBER))
|
|
||||||
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
|
|
||||||
.distinct(EventEntityFields.STATE_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
|
||||||
val roomMembersEvents = changeSet.insertions
|
|
||||||
.asSequence()
|
|
||||||
.mapNotNull { results[it]?.eventId }
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
val taskParams = UpdateUserTask.Params(roomMembersEvents)
|
|
||||||
updateUserTask
|
|
||||||
.configureWith(taskParams)
|
|
||||||
.executeOn(TaskThread.IO)
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -26,7 +26,4 @@ internal abstract class UserModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindUserService(userService: DefaultUserService): UserService
|
abstract fun bindUserService(userService: DefaultUserService): UserService
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindUpdateUserTask(updateUserTask: DefaultUpdateUserTask): UpdateUserTask
|
|
||||||
|
|
||||||
}
|
}
|
@ -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"
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
@ -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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 = "")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
22
vector/src/main/res/drawable/ic_search.xml
Normal file
22
vector/src/main/res/drawable/ic_search.xml
Normal 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>
|
@ -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" />
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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" />
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user