Merge pull request #459 from vector-im/feature/clenup_after_hol

Review of merged PRs
This commit is contained in:
Benoit Marty 2019-08-06 18:39:37 +02:00 committed by GitHub
commit d9f448c9aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 263 additions and 400 deletions

View File

@ -149,7 +149,7 @@ object MatrixPatterns {
return null return null
} }


val index = matrixId.lastIndexOf(":") val index = matrixId.indexOf(":")


return if (index == -1) { return if (index == -1) {
null null

View File

@ -82,8 +82,13 @@ data class Event(
) { ) {




@Transient
var mxDecryptionResult: OlmDecryptionResult? = null var mxDecryptionResult: OlmDecryptionResult? = null

@Transient
var mCryptoError: MXCryptoError.ErrorType? = null var mCryptoError: MXCryptoError.ErrorType? = null

@Transient
var sendState: SendState = SendState.UNKNOWN var sendState: SendState = SendState.UNKNOWN




@ -99,42 +104,6 @@ data class Event(
// Crypto // Crypto
//============================================================================================================== //==============================================================================================================


// /**
// * For encrypted events, the plaintext payload for the event.
// * This is a small MXEvent instance with typically value for `type` and 'content' fields.
// */
// @Transient
// var mClearEvent: Event? = null
// private set
//
// /**
// * Curve25519 key which we believe belongs to the sender of the event.
// * See `senderKey` property.
// */
// @Transient
// private var mSenderCurve25519Key: String? = null
//
// /**
// * Ed25519 key which the sender of this event (for olm) or the creator of the megolm session (for megolm) claims to own.
// * See `claimedEd25519Key` property.
// */
// @Transient
// private var mClaimedEd25519Key: String? = null
//
// /**
// * Curve25519 keys of devices involved in telling us about the senderCurve25519Key and claimedEd25519Key.
// * See `forwardingCurve25519KeyChain` property.
// */
// @Transient
// private var mForwardingCurve25519KeyChain: List<String> = ArrayList()
//
// /**
// * Decryption error
// */
// @Transient
// var mCryptoError: MXCryptoError? = null
// private set

/** /**
* @return true if this event is encrypted. * @return true if this event is encrypted.
*/ */
@ -142,51 +111,11 @@ data class Event(
return TextUtils.equals(type, EventType.ENCRYPTED) return TextUtils.equals(type, EventType.ENCRYPTED)
} }


/**
* Update the clear data on this event.
* This is used after decrypting an event; it should not be used by applications.
*
* @param decryptionResult the decryption result, including the plaintext and some key info.
*/
// internal fun setClearData(decryptionResult: MXEventDecryptionResult?) {
// mClearEvent = null
// if (decryptionResult != null) {
// if (decryptionResult.clearEvent != null) {
// val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java)
// mClearEvent = adapter.fromJsonValue(decryptionResult.clearEvent)
//
// if (mClearEvent != null) {
// mSenderCurve25519Key = decryptionResult.senderCurve25519Key
// mClaimedEd25519Key = decryptionResult.claimedEd25519Key
// mForwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
//
// // For encrypted events with relation, the m.relates_to is kept in clear, so we need to put it back
// // in the clear event
// try {
// content?.get("m.relates_to")?.let { clearRelates ->
// mClearEvent = mClearEvent?.copy(
// content = HashMap(mClearEvent!!.content).apply {
// this["m.relates_to"] = clearRelates
// }
// )
// }
// } catch (e: Exception) {
// Timber.e(e, "Unable to restore 'm.relates_to' the clear event")
// }
// }
//
//
// }
// }
// mCryptoError = null
// }

/** /**
* @return The curve25519 key that sent this event. * @return The curve25519 key that sent this event.
*/ */
fun getSenderKey(): String? { fun getSenderKey(): String? {
return mxDecryptionResult?.senderKey return mxDecryptionResult?.senderKey
// return mClearEvent?.mSenderCurve25519Key ?: mSenderCurve25519Key
} }


/** /**
@ -194,23 +123,13 @@ data class Event(
*/ */
fun getKeysClaimed(): Map<String, String> { fun getKeysClaimed(): Map<String, String> {
return mxDecryptionResult?.keysClaimed ?: HashMap() return mxDecryptionResult?.keysClaimed ?: HashMap()
// val res = HashMap<String, String>()
//
// val claimedEd25519Key = if (null != mClearEvent) mClearEvent!!.mClaimedEd25519Key else mClaimedEd25519Key
//
// if (null != claimedEd25519Key) {
// res["ed25519"] = claimedEd25519Key
// }
//
// return res
} }
//
/** /**
* @return the event type * @return the event type
*/ */
fun getClearType(): String { fun getClearType(): String {
return mxDecryptionResult?.payload?.get("type")?.toString() return mxDecryptionResult?.payload?.get("type")?.toString() ?: type
?: type//get("type")?.toString() ?: type
} }


/** /**
@ -220,30 +139,8 @@ data class Event(
return mxDecryptionResult?.payload?.get("content") as? Content ?: content return mxDecryptionResult?.payload?.get("content") as? Content ?: content
} }


// /**
// * @return the linked crypto error
// */
// fun getCryptoError(): MXCryptoError? {
// return mCryptoError
// }
//
// /**
// * Update the linked crypto error
// *
// * @param error the new crypto error.
// */
// fun setCryptoError(error: MXCryptoError?) {
// mCryptoError = error
// if (null != error) {
// mClearEvent = null
// }
// }


fun toContentStringWithIndent(): String { fun toContentStringWithIndent(): String {
val contentMap = this.toContent()?.toMutableMap() ?: HashMap() val contentMap = toContent()?.toMutableMap() ?: HashMap()
contentMap.remove("mxDecryptionResult")
contentMap.remove("mCryptoError")
return JSONObject(contentMap).toString(4) return JSONObject(contentMap).toString(4)
} }


@ -302,31 +199,19 @@ data class Event(




fun Event.isTextMessage(): Boolean { fun Event.isTextMessage(): Boolean {
if (this.getClearType() == EventType.MESSAGE) { return getClearType() == EventType.MESSAGE
return getClearContent()?.toModel<MessageContent>()?.let { && when (getClearContent()?.toModel<MessageContent>()?.type) {
when (it.type) { MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_NOTICE -> true
MessageType.MSGTYPE_NOTICE -> { else -> false
true
}
else -> false
}
} ?: false
} }
return false
} }


fun Event.isImageMessage(): Boolean { fun Event.isImageMessage(): Boolean {
if (this.getClearType() == EventType.MESSAGE) { return getClearType() == EventType.MESSAGE
return getClearContent()?.toModel<MessageContent>()?.let { && when (getClearContent()?.toModel<MessageContent>()?.type) {
when (it.type) { MessageType.MSGTYPE_IMAGE -> true
MessageType.MSGTYPE_IMAGE -> { else -> false
true
}
else -> false
}
} ?: false
} }
return false
} }

View File

@ -27,7 +27,7 @@ internal interface SessionParamsStore {


fun getAll(): List<SessionParams> fun getAll(): List<SessionParams>


fun save(sessionParams: SessionParams): Try<SessionParams> fun save(sessionParams: SessionParams): Try<Unit>


fun delete(userId: String): Try<Unit> fun delete(userId: String): Try<Unit>



View File

@ -62,7 +62,7 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
return sessionParams return sessionParams
} }


override fun save(sessionParams: SessionParams): Try<SessionParams> { override fun save(sessionParams: SessionParams): Try<Unit> {
return Try { return Try {
val entity = mapper.map(sessionParams) val entity = mapper.map(sessionParams)
if (entity != null) { if (entity != null) {
@ -72,7 +72,6 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
} }
realm.close() realm.close()
} }
sessionParams
} }
} }



View File

@ -31,3 +31,11 @@ inline fun <A> TryOf<A>.onError(f: (Throwable) -> Unit): Try<A> = fix()
fun <A> Try<A>.foldToCallback(callback: MatrixCallback<A>): Unit = fold( fun <A> Try<A>.foldToCallback(callback: MatrixCallback<A>): Unit = fold(
{ callback.onFailure(it) }, { callback.onFailure(it) },
{ callback.onSuccess(it) }) { callback.onSuccess(it) })

/**
* Same as doOnNext for Observables
*/
inline fun <A> Try<A>.alsoDo(f: (A) -> Unit) = map {
f(it)
it
}

View File

@ -115,7 +115,7 @@ internal class DefaultRelationService @Inject constructor(private val context: C
eventId, eventId,
reason) reason)
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData) return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData, true)
} }


override fun editTextMessage(targetEventId: String, override fun editTextMessage(targetEventId: String,
@ -199,14 +199,13 @@ internal class DefaultRelationService @Inject constructor(private val context: C
// Same parameter // Same parameter
val params = EncryptEventWorker.Params(credentials.userId, roomId, event, keepKeys) val params = EncryptEventWorker.Params(credentials.userId, roomId, event, keepKeys)
val sendWorkData = WorkerParamsFactory.toData(params) val sendWorkData = WorkerParamsFactory.toData(params)
return TimelineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData) return TimelineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true)
} }


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


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

View File

@ -22,10 +22,7 @@ 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.content.ContentAttachmentData import im.vector.matrix.android.api.session.content.ContentAttachmentData
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.*
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.send.SendService
@ -41,9 +38,11 @@ import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.content.UploadContentWorker
import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.CancelableWork
import im.vector.matrix.android.internal.worker.AlwaysSuccessfulWorker
import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil
import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.startChain
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -82,11 +81,11 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
return if (cryptoService.isRoomEncrypted(roomId)) { return if (cryptoService.isRoomEncrypted(roomId)) {
Timber.v("Send event in encrypted room") Timber.v("Send event in encrypted room")
val encryptWork = createEncryptEventWork(event, true) val encryptWork = createEncryptEventWork(event, true)
val sendWork = createSendEventWork(event) val sendWork = createSendEventWork(event, false)
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, sendWork) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, sendWork)
CancelableWork(context, encryptWork.id) CancelableWork(context, encryptWork.id)
} else { } else {
val sendWork = createSendEventWork(event) val sendWork = createSendEventWork(event, true)
TimelineSendEventWorkCommon.postWork(context, roomId, sendWork) TimelineSendEventWorkCommon.postWork(context, roomId, sendWork)
CancelableWork(context, sendWork.id) CancelableWork(context, sendWork.id)
} }
@ -106,7 +105,7 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
} }


override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
if (localEcho.root.isTextMessage()) { if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) {
return sendEvent(localEcho.root) return sendEvent(localEcho.root)
} }
return null return null
@ -114,7 +113,8 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
} }


override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? { override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? {
//TODO this need a refactoring of attachement sending if (localEcho.root.isImageMessage() && localEcho.root.sendState.hasFailed()) {
//TODO this need a refactoring of attachement sending
// val clearContent = localEcho.root.getClearContent() // val clearContent = localEcho.root.getClearContent()
// val messageContent = clearContent?.toModel<MessageContent>() ?: return null // val messageContent = clearContent?.toModel<MessageContent>() ?: return null
// when (messageContent.type) { // when (messageContent.type) {
@ -143,8 +143,9 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
// } // }
// } // }
// } // }
return null
}
return null return null

} }


override fun deleteFailedEcho(localEcho: TimelineEvent) { override fun deleteFailedEcho(localEcho: TimelineEvent) {
@ -162,15 +163,16 @@ internal class DefaultSendService @Inject constructor(private val context: Conte


override fun clearSendingQueue() { override fun clearSendingQueue() {
TimelineSendEventWorkCommon.cancelAllWorks(context, roomId) TimelineSendEventWorkCommon.cancelAllWorks(context, roomId)
WorkManager.getInstance(context).cancelUniqueWork(buildWorkIdentifier(UPLOAD_WORK)) WorkManager.getInstance(context).cancelUniqueWork(buildWorkName(UPLOAD_WORK))


matrixOneTimeWorkRequestBuilder<FakeSendWorker>() // Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied
matrixOneTimeWorkRequestBuilder<AlwaysSuccessfulWorker>()
.build().let { .build().let {
TimelineSendEventWorkCommon.postWork(context, roomId, it, ExistingWorkPolicy.REPLACE) TimelineSendEventWorkCommon.postWork(context, roomId, it, ExistingWorkPolicy.REPLACE)


//need to clear also image sending queue //need to clear also image sending queue
WorkManager.getInstance(context) WorkManager.getInstance(context)
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it) .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it)
.enqueue() .enqueue()
} }


@ -244,26 +246,26 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId) val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId)


val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true) val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true)
val sendWork = createSendEventWork(localEcho) val sendWork = createSendEventWork(localEcho, false)


if (isRoomEncrypted) { if (isRoomEncrypted) {
val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/) val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/)


val op: Operation = WorkManager.getInstance(context) val op: Operation = WorkManager.getInstance(context)
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
.then(encryptWork) .then(encryptWork)
.then(sendWork) .then(sendWork)
.enqueue() .enqueue()
op.result.addListener(Runnable { op.result.addListener(Runnable {
if (op.result.isCancelled) { if (op.result.isCancelled) {
Timber.e("CHAINE WAS CANCELLED") Timber.e("CHAIN WAS CANCELLED")
} else if (op.state.value is Operation.State.FAILURE) { } else if (op.state.value is Operation.State.FAILURE) {
Timber.e("CHAINE DID FAIL") Timber.e("CHAIN DID FAIL")
} }
}, workerFutureListenerExecutor) }, workerFutureListenerExecutor)
} else { } else {
WorkManager.getInstance(context) WorkManager.getInstance(context)
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
.then(sendWork) .then(sendWork)
.enqueue() .enqueue()
} }
@ -275,11 +277,11 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
localEchoEventFactory.saveLocalEcho(monarchy, event) localEchoEventFactory.saveLocalEcho(monarchy, event)
} }


private fun buildWorkIdentifier(identifier: String): String { private fun buildWorkName(identifier: String): String {
return "${roomId}_$identifier" return "${roomId}_$identifier"
} }


private fun createEncryptEventWork(event: Event, startChain: Boolean = false): OneTimeWorkRequest { private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
// Same parameter // Same parameter
val params = EncryptEventWorker.Params(credentials.userId, roomId, event) val params = EncryptEventWorker.Params(credentials.userId, roomId, event)
val sendWorkData = WorkerParamsFactory.toData(params) val sendWorkData = WorkerParamsFactory.toData(params)
@ -287,20 +289,16 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
return matrixOneTimeWorkRequestBuilder<EncryptEventWorker>() return matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
.setConstraints(WorkManagerUtil.workConstraints) .setConstraints(WorkManagerUtil.workConstraints)
.setInputData(sendWorkData) .setInputData(sendWorkData)
.apply { .startChain(startChain)
if (startChain) {
setInputMerger(NoMerger::class.java)
}
}
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build() .build()
} }


private fun createSendEventWork(event: Event): OneTimeWorkRequest { private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)


return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData) return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
} }


private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
@ -309,23 +307,19 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
} }
val sendContentWorkerParams = RedactEventWorker.Params(credentials.userId, redactEvent.eventId!!, roomId, event.eventId, reason) val sendContentWorkerParams = RedactEventWorker.Params(credentials.userId, redactEvent.eventId!!, roomId, event.eventId, reason)
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData) return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData, true)
} }


private fun createUploadMediaWork(event: Event, private fun createUploadMediaWork(event: Event,
attachment: ContentAttachmentData, attachment: ContentAttachmentData,
isRoomEncrypted: Boolean, isRoomEncrypted: Boolean,
startChain: Boolean = false): OneTimeWorkRequest { startChain: Boolean): OneTimeWorkRequest {
val uploadMediaWorkerParams = UploadContentWorker.Params(credentials.userId, roomId, event, attachment, isRoomEncrypted) val uploadMediaWorkerParams = UploadContentWorker.Params(credentials.userId, roomId, event, attachment, isRoomEncrypted)
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)


return matrixOneTimeWorkRequestBuilder<UploadContentWorker>() return matrixOneTimeWorkRequestBuilder<UploadContentWorker>()
.setConstraints(WorkManagerUtil.workConstraints) .setConstraints(WorkManagerUtil.workConstraints)
.apply { .startChain(startChain)
if (startChain) {
setInputMerger(NoMerger::class.java)
}
}
.setInputData(uploadWorkData) .setInputData(uploadWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build() .build()

View File

@ -18,7 +18,10 @@ package im.vector.matrix.android.internal.session.room.send
import androidx.work.Data import androidx.work.Data
import androidx.work.InputMerger import androidx.work.InputMerger


class NoMerger : InputMerger() { /**
* InputMerger which takes only the first input, to ensure an appended work will only have the specified parameters
*/
internal class NoMerger : InputMerger() {
override fun merge(inputs: MutableList<Data>): Data { override fun merge(inputs: MutableList<Data>): Data {
return inputs.first() return inputs.first()
} }

View File

@ -19,6 +19,7 @@ import android.content.Context
import androidx.work.* import androidx.work.*
import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil
import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
import im.vector.matrix.android.internal.worker.startChain
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit




@ -41,7 +42,7 @@ internal object TimelineSendEventWorkCommon {
else -> { else -> {
val firstWork = workRequests.first() val firstWork = workRequests.first()
var continuation = WorkManager.getInstance(context) var continuation = WorkManager.getInstance(context)
.beginUniqueWork(buildWorkIdentifier(roomId), ExistingWorkPolicy.APPEND, firstWork) .beginUniqueWork(buildWorkName(roomId), ExistingWorkPolicy.APPEND, firstWork)
for (i in 1 until workRequests.size) { for (i in 1 until workRequests.size) {
val workRequest = workRequests[i] val workRequest = workRequests[i]
continuation = continuation.then(workRequest) continuation = continuation.then(workRequest)
@ -53,23 +54,24 @@ internal object TimelineSendEventWorkCommon {


fun postWork(context: Context, roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND) { fun postWork(context: Context, roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND) {
WorkManager.getInstance(context) WorkManager.getInstance(context)
.beginUniqueWork(buildWorkIdentifier(roomId), policy, workRequest) .beginUniqueWork(buildWorkName(roomId), policy, workRequest)
.enqueue() .enqueue()
} }


inline fun <reified W : ListenableWorker> createWork(data: Data): OneTimeWorkRequest { inline fun <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
return matrixOneTimeWorkRequestBuilder<W>() return matrixOneTimeWorkRequestBuilder<W>()
.setConstraints(WorkManagerUtil.workConstraints) .setConstraints(WorkManagerUtil.workConstraints)
.startChain(startChain)
.setInputData(data) .setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build() .build()
} }


private fun buildWorkIdentifier(roomId: String): String { private fun buildWorkName(roomId: String): String {
return "${roomId}_$SEND_WORK" return "${roomId}_$SEND_WORK"
} }


fun cancelAllWorks(context: Context, roomId: String) { fun cancelAllWorks(context: Context, roomId: String) {
WorkManager.getInstance(context).cancelUniqueWork(buildWorkIdentifier(roomId)) WorkManager.getInstance(context).cancelUniqueWork(buildWorkName(roomId))
} }
} }

View File

@ -36,6 +36,6 @@ internal abstract class AccountDataModule {
} }


@Binds @Binds
abstract fun bindUpdateUserAccountDataTask(updateUserAccountDataTask: DefaultUpdateUserAcountDataTask): UpdateUserAccountDataTask abstract fun bindUpdateUserAccountDataTask(updateUserAccountDataTask: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask


} }

View File

@ -41,8 +41,8 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa


} }


internal class DefaultUpdateUserAcountDataTask @Inject constructor(private val accountDataApi: AccountDataAPI, internal class DefaultUpdateUserAccountDataTask @Inject constructor(private val accountDataApi: AccountDataAPI,
private val credentials: Credentials) : UpdateUserAccountDataTask { private val credentials: Credentials) : UpdateUserAccountDataTask {


override suspend fun execute(params: UpdateUserAccountDataTask.Params) { override suspend fun execute(params: UpdateUserAccountDataTask.Params) {
return executeRequest { return executeRequest {

View File

@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package im.vector.matrix.android.internal.session.room.send package im.vector.matrix.android.internal.worker


import android.content.Context import android.content.Context
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters


internal class FakeSendWorker(context: Context, params: WorkerParameters) internal class AlwaysSuccessfulWorker(context: Context, params: WorkerParameters)
: Worker(context, params) { : Worker(context, params) {


override fun doWork(): Result { override fun doWork(): Result {

View File

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

import androidx.work.OneTimeWorkRequest
import im.vector.matrix.android.internal.session.room.send.NoMerger

/**
* If startChain parameter is true, the builder will have a inputMerger set to [NoMerger]
*/
internal fun OneTimeWorkRequest.Builder.startChain(startChain: Boolean): OneTimeWorkRequest.Builder {
if (startChain) {
setInputMerger(NoMerger::class.java)
}
return this
}

View File

@ -31,15 +31,14 @@ class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) {


fun toHumanReadable(throwable: Throwable?): String { fun toHumanReadable(throwable: Throwable?): String {
return when (throwable) { return when (throwable) {
null -> "" null -> null
is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network) is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network)
is Failure.ServerError -> { is Failure.ServerError -> {
throwable.error.message.takeIf { it.isNotEmpty() } throwable.error.message.takeIf { it.isNotEmpty() }
?: throwable.error.code.takeIf { it.isNotEmpty() } ?: throwable.error.code.takeIf { it.isNotEmpty() }
?: stringProvider.getString(R.string.unknown_error)
} }
else -> throwable.localizedMessage else -> throwable.localizedMessage
?: stringProvider.getString(R.string.unknown_error)
} }
?: stringProvider.getString(R.string.unknown_error)
} }
} }

View File

@ -18,25 +18,22 @@ package im.vector.riotx.core.extensions


import android.text.Editable import android.text.Editable
import android.text.InputType import android.text.InputType
import android.text.TextWatcher
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.EditText import android.widget.EditText
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.SimpleTextWatcher


fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_filter, fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_filter,
@DrawableRes clearIconRes: Int = R.drawable.ic_x_green) { @DrawableRes clearIconRes: Int = R.drawable.ic_x_green) {


addTextChangedListener(object : TextWatcher { addTextChangedListener(object : SimpleTextWatcher() {
override fun afterTextChanged(editable: Editable?) { override fun afterTextChanged(s: Editable) {
val clearIcon = if (editable?.isNotEmpty() == true) clearIconRes else 0 val clearIcon = if (s.isNotEmpty()) clearIconRes else 0
setCompoundDrawablesWithIntrinsicBounds(searchIconRes, 0, clearIcon, 0) setCompoundDrawablesWithIntrinsicBounds(searchIconRes, 0, clearIcon, 0)
} }

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
}) })


maxLines = 1 maxLines = 1

View File

@ -19,6 +19,7 @@ package im.vector.riotx.core.platform
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.configuration.VectorConfiguration
import timber.log.Timber import timber.log.Timber
@ -46,7 +47,7 @@ class ConfigurationViewModel @Inject constructor(
if (newHash != currentConfigurationValue) { if (newHash != currentConfigurationValue) {
Timber.v("Configuration: recreate the Activity") Timber.v("Configuration: recreate the Activity")
currentConfigurationValue = newHash currentConfigurationValue = newHash
_activityRestarter.postValue(LiveEvent(Unit)) _activityRestarter.postLiveEvent(Unit)
} }
} }
} }

View File

@ -19,6 +19,7 @@ package im.vector.riotx.core.platform
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher



/** /**
* TextWatcher with default no op implementation * TextWatcher with default no op implementation
*/ */

View File

@ -24,11 +24,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.Async import com.airbnb.mvrx.*
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.viewModel
import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
@ -39,7 +35,6 @@ import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.SimpleFragmentActivity
import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.platform.WaitingViewData
import kotlinx.android.synthetic.main.activity.* import kotlinx.android.synthetic.main.activity.*
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject


class CreateDirectRoomActivity : SimpleFragmentActivity() { class CreateDirectRoomActivity : SimpleFragmentActivity() {
@ -98,7 +93,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
} else } else
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setMessage(errorFormatter.toHumanReadable(error)) .setMessage(errorFormatter.toHumanReadable(error))
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } .setPositiveButton(R.string.ok, null)
.show() .show()
} }



View File

@ -21,7 +21,9 @@ package im.vector.riotx.features.home.createdirect
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import arrow.core.Option import arrow.core.Option
import com.airbnb.mvrx.* import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
@ -33,10 +35,8 @@ import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.BiFunction
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit


private typealias KnowUsersFilter = String private typealias KnowUsersFilter = String
@ -103,7 +103,6 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
.execute { .execute {
copy(createAndInviteState = it) copy(createAndInviteState = it)
} }
.disposeOnClear()
} }


private fun handleRemoveSelectedUser(action: CreateDirectRoomActions.RemoveSelectedUser) = withState { state -> private fun handleRemoveSelectedUser(action: CreateDirectRoomActions.RemoveSelectedUser) = withState { state ->

View File

@ -300,8 +300,9 @@ class RoomDetailFragment :
composerLayout.collapse() composerLayout.collapse()
} }


private fun enterSpecialMode(event: TimelineEvent, @DrawableRes private fun enterSpecialMode(event: TimelineEvent,
iconRes: Int, useText: Boolean) { @DrawableRes iconRes: Int,
useText: Boolean) {
commandAutocompletePolicy.enabled = false commandAutocompletePolicy.enabled = false
//switch to expanded bar //switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply { composerLayout.composerRelatedMessageTitle.apply {
@ -820,106 +821,101 @@ class RoomDetailFragment :
textComposerViewModel.process(TextComposerActions.QueryUsers(query)) textComposerViewModel.process(TextComposerActions.QueryUsers(query))
} }


private fun handleActions(actionData: ActionsHandler.ActionData) { private fun handleActions(action: SimpleAction) {
when (actionData.actionId) { when (action) {
MessageMenuViewModel.ACTION_ADD_REACTION -> { is SimpleAction.AddReaction -> {
val eventId = actionData.data?.toString() ?: return startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), eventId), REACTION_SELECT_REQUEST_CODE)
} }
MessageMenuViewModel.ACTION_VIEW_REACTIONS -> { is SimpleAction.ViewReactions -> {
val messageInformationData = actionData.data as? MessageInformationData ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
?: return
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
} }
MessageMenuViewModel.ACTION_COPY -> { is SimpleAction.Copy -> {
//I need info about the current selected message :/ //I need info about the current selected message :/
copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false) copyToClipboard(requireContext(), action.content, false)
val msg = requireContext().getString(R.string.copied_to_clipboard) val msg = requireContext().getString(R.string.copied_to_clipboard)
showSnackWithMessage(msg, Snackbar.LENGTH_SHORT) showSnackWithMessage(msg, Snackbar.LENGTH_SHORT)
} }
MessageMenuViewModel.ACTION_DELETE -> { is SimpleAction.Delete -> {
val eventId = actionData.data?.toString() ?: return roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason)))
roomDetailViewModel.process(RoomDetailActions.RedactAction(eventId, context?.getString(R.string.event_redacted_by_user_reason)))
} }
MessageMenuViewModel.ACTION_SHARE -> { is SimpleAction.Share -> {
//TODO current data communication is too limited //TODO current data communication is too limited
//Need to now the media type //Need to now the media type
actionData.data?.toString()?.let { //TODO bad, just POC
//TODO bad, just POC BigImageViewer.imageLoader().loadImage(
BigImageViewer.imageLoader().loadImage( action.hashCode(),
actionData.hashCode(), Uri.parse(action.imageUrl),
Uri.parse(it), object : ImageLoader.Callback {
object : ImageLoader.Callback { override fun onFinish() {}
override fun onFinish() {}

override fun onSuccess(image: File?) {
if (image != null)
shareMedia(requireContext(), image, "image/*")
}

override fun onFail(error: Exception?) {}

override fun onCacheHit(imageType: Int, image: File?) {}

override fun onCacheMiss(imageType: Int, image: File?) {}

override fun onProgress(progress: Int) {}

override fun onStart() {}


override fun onSuccess(image: File?) {
if (image != null)
shareMedia(requireContext(), image, "image/*")
} }


) override fun onFail(error: Exception?) {}
}
override fun onCacheHit(imageType: Int, image: File?) {}

override fun onCacheMiss(imageType: Int, image: File?) {}

override fun onProgress(progress: Int) {}

override fun onStart() {}

}
)
} }
MessageMenuViewModel.VIEW_SOURCE, is SimpleAction.ViewSource -> {
MessageMenuViewModel.VIEW_DECRYPTED_SOURCE -> {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
view.findViewById<TextView>(R.id.event_content_text_view)?.let { view.findViewById<TextView>(R.id.event_content_text_view)?.let {
it.text = actionData.data?.toString() ?: "" it.text = action.content
} }


AlertDialog.Builder(requireActivity()) AlertDialog.Builder(requireActivity())
.setView(view) .setView(view)
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } .setPositiveButton(R.string.ok, null)
.show() .show()
} }
MessageMenuViewModel.ACTION_QUICK_REACT -> { is SimpleAction.ViewDecryptedSource -> {
//eventId,ClickedOn,Add val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
(actionData.data as? Triple<String, String, Boolean>)?.let { (eventId, clickedOn, add) -> view.findViewById<TextView>(R.id.event_content_text_view)?.let {
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, add)) it.text = action.content
} }

AlertDialog.Builder(requireActivity())
.setView(view)
.setPositiveButton(R.string.ok, null)
.show()
} }
MessageMenuViewModel.ACTION_EDIT -> { is SimpleAction.QuickReact -> {
val eventId = actionData.data.toString() //eventId,ClickedOn,Add
roomDetailViewModel.process(RoomDetailActions.EnterEditMode(eventId)) roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
} }
MessageMenuViewModel.ACTION_QUOTE -> { is SimpleAction.Edit -> {
val eventId = actionData.data.toString() roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId))
roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(eventId))
} }
MessageMenuViewModel.ACTION_REPLY -> { is SimpleAction.Quote -> {
val eventId = actionData.data.toString() roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId))
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
} }
MessageMenuViewModel.ACTION_COPY_PERMALINK -> { is SimpleAction.Reply -> {
val eventId = actionData.data.toString() roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId))
val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, eventId) }
is SimpleAction.CopyPermalink -> {
val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId)
copyToClipboard(requireContext(), permalink, false) copyToClipboard(requireContext(), permalink, false)
showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)


} }
MessageMenuViewModel.ACTION_RESEND -> { is SimpleAction.Resend -> {
val eventId = actionData.data.toString() roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId))
roomDetailViewModel.process(RoomDetailActions.ResendMessage(eventId))
} }
MessageMenuViewModel.ACTION_REMOVE -> { is SimpleAction.Remove -> {
val eventId = actionData.data.toString() roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId))
roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(eventId))
} }
else -> { else -> {
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show() Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
} }
} }
} }

View File

@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.detail.timeline.action


import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
import javax.inject.Inject import javax.inject.Inject


@ -25,15 +26,10 @@ import javax.inject.Inject
*/ */
class ActionsHandler @Inject constructor() : ViewModel() { class ActionsHandler @Inject constructor() : ViewModel() {


data class ActionData( val actionCommandEvent = MutableLiveData<LiveEvent<SimpleAction>>()
val actionId: String,
val data: Any?
)


val actionCommandEvent = MutableLiveData<LiveEvent<ActionData>>() fun fireAction(action: SimpleAction) {

actionCommandEvent.postLiveEvent(action)
fun fireAction(actionId: String, data: Any? = null) {
actionCommandEvent.value = LiveEvent(ActionData(actionId,data))
} }


} }

View File

@ -89,7 +89,7 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
} }
menuActionFragment.interactionListener = object : MessageMenuFragment.InteractionListener { menuActionFragment.interactionListener = object : MessageMenuFragment.InteractionListener {
override fun didSelectMenuAction(simpleAction: SimpleAction) { override fun didSelectMenuAction(simpleAction: SimpleAction) {
actionHandlerModel.fireAction(simpleAction.uid, simpleAction.data) actionHandlerModel.fireAction(simpleAction)
dismiss() dismiss()
} }
} }
@ -105,7 +105,7 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener { quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener {


override fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String) { override fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String) {
actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Triple(eventId, clickedOn, add)) actionHandlerModel.fireAction(SimpleAction.QuickReact(eventId, clickedOn, add))
dismiss() dismiss()
} }
} }

View File

@ -15,6 +15,8 @@
*/ */
package im.vector.riotx.features.home.room.detail.timeline.action package im.vector.riotx.features.home.room.detail.timeline.action


import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
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
@ -36,7 +38,24 @@ import im.vector.riotx.core.utils.isSingleEmoji
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData




data class SimpleAction(val uid: String, val titleRes: Int, val iconResId: Int?, val data: Any? = null) sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) {
data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction)
data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy)
data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit)
data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote)
data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply)
data class Share(val imageUrl: String?) : SimpleAction(R.string.share, R.drawable.ic_share)
data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw)
data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash)
data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete)
data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round)
data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source)
data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source)
data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink)
data class Flag(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag)
data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0)
data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions)
}


data class MessageMenuState( data class MessageMenuState(
val roomId: String, val roomId: String,
@ -68,24 +87,6 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
private val informationData: MessageInformationData = initialState.informationData private val informationData: MessageInformationData = initialState.informationData


companion object : MvRxViewModelFactory<MessageMenuViewModel, MessageMenuState> { companion object : MvRxViewModelFactory<MessageMenuViewModel, MessageMenuState> {

const val ACTION_ADD_REACTION = "add_reaction"
const val ACTION_COPY = "copy"
const val ACTION_EDIT = "edit"
const val ACTION_QUOTE = "quote"
const val ACTION_REPLY = "reply"
const val ACTION_SHARE = "share"
const val ACTION_RESEND = "resend"
const val ACTION_REMOVE = "remove"
const val ACTION_DELETE = "delete"
const val ACTION_CANCEL = "cancel"
const val VIEW_SOURCE = "VIEW_SOURCE"
const val VIEW_DECRYPTED_SOURCE = "VIEW_DECRYPTED_SOURCE"
const val ACTION_COPY_PERMALINK = "ACTION_COPY_PERMALINK"
const val ACTION_FLAG = "ACTION_FLAG"
const val ACTION_QUICK_REACT = "ACTION_QUICK_REACT"
const val ACTION_VIEW_REACTIONS = "ACTION_VIEW_REACTIONS"

override fun create(viewModelContext: ViewModelContext, state: MessageMenuState): MessageMenuViewModel? { override fun create(viewModelContext: ViewModelContext, state: MessageMenuState): MessageMenuViewModel? {
val fragment: MessageMenuFragment = (viewModelContext as FragmentViewModelContext).fragment() val fragment: MessageMenuFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.messageMenuViewModelFactory.create(state) return fragment.messageMenuViewModelFactory.create(state)
@ -99,75 +100,64 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
private fun observeEvent() { private fun observeEvent() {
RxRoom(room) RxRoom(room)
.liveTimelineEvent(eventId) .liveTimelineEvent(eventId)
?.map { .map {
actionsForEvent(it) actionsForEvent(it)
} }
?.execute { .execute {
copy(actions = it) copy(actions = it)
} }
} }


private fun actionsForEvent(event: TimelineEvent): List<SimpleAction> { private fun actionsForEvent(event: TimelineEvent): List<SimpleAction> {

val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel() val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
?: event.root.getClearContent().toModel() ?: event.root.getClearContent().toModel()
val type = messageContent?.type val type = messageContent?.type


return if (event.root.sendState.hasFailed()) { return arrayListOf<SimpleAction>().apply {
arrayListOf<SimpleAction>().apply { if (event.root.sendState.hasFailed()) {
if (canRetry(event)) { if (canRetry(event)) {
this.add(SimpleAction(ACTION_RESEND, R.string.global_retry, R.drawable.ic_refresh_cw, eventId)) add(SimpleAction.Resend(eventId))
} }
this.add(SimpleAction(ACTION_REMOVE, R.string.remove, R.drawable.ic_trash, eventId)) add(SimpleAction.Remove(eventId))
} } else if (event.root.sendState.isSending()) {
} else if (event.root.sendState.isSending()) { //TODO is uploading attachment?
//TODO is uploading attachment?
arrayListOf<SimpleAction>().apply {
if (canCancel(event)) { if (canCancel(event)) {
this.add(SimpleAction(ACTION_CANCEL, R.string.cancel, R.drawable.ic_close_round, eventId)) add(SimpleAction.Cancel(eventId))
} }
} } else {
} else {
arrayListOf<SimpleAction>().apply {

if (!event.root.isRedacted()) { if (!event.root.isRedacted()) {

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


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


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


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


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


if (canQuote(event, messageContent)) { if (canQuote(event, messageContent)) {
add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId)) add(SimpleAction.Quote(eventId))
} }


if (canViewReactions(event)) { if (canViewReactions(event)) {
add(SimpleAction(ACTION_VIEW_REACTIONS, R.string.message_view_reaction, R.drawable.ic_view_reactions, informationData)) add(SimpleAction.ViewReactions(informationData))
} }


if (canShare(type)) { if (canShare(type)) {
if (messageContent is MessageImageContent) { if (messageContent is MessageImageContent) {
add( add(SimpleAction.Share(session.contentUrlResolver().resolveFullSize(messageContent.url)))
SimpleAction(ACTION_SHARE,
R.string.share, R.drawable.ic_share,
session.contentUrlResolver().resolveFullSize(messageContent.url))
)
} }
//TODO //TODO
} }
@ -181,17 +171,17 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
} }
} }


add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, event.root.toContentStringWithIndent())) add(SimpleAction.ViewSource(event.root.toContentStringWithIndent()))
if (event.isEncrypted()) { if (event.isEncrypted()) {
val decryptedContent = event.root.toClearContentStringWithIndent() val decryptedContent = event.root.toClearContentStringWithIndent()
?: stringProvider.getString(R.string.encryption_information_decryption_error) ?: stringProvider.getString(R.string.encryption_information_decryption_error)
add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, decryptedContent)) add(SimpleAction.ViewDecryptedSource(decryptedContent))
} }
add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, event.root.eventId)) add(SimpleAction.CopyPermalink(eventId))


if (session.myUserId != 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
add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, event.root.eventId)) add(SimpleAction.Flag(eventId))
} }
} }
} }
@ -269,9 +259,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_EMOTE,
MessageType.FORMAT_MATRIX_HTML, MessageType.FORMAT_MATRIX_HTML,
MessageType.MSGTYPE_LOCATION -> { MessageType.MSGTYPE_LOCATION -> true
true
}
else -> false else -> false
} }
} }
@ -281,9 +269,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
return when (type) { return when (type) {
MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_VIDEO -> { MessageType.MSGTYPE_VIDEO -> true
true
}
else -> false else -> false
} }
} }

View File

@ -166,11 +166,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
root.isClickable = informationData.sendState.isSent() root.isClickable = informationData.sendState.isSent()
val state = if (informationData.hasPendingEdits) SendState.UNSENT else informationData.sendState val state = if (informationData.hasPendingEdits) SendState.UNSENT else informationData.sendState
textView?.setTextColor(colorProvider.getMessageTextColor(state)) textView?.setTextColor(colorProvider.getMessageTextColor(state))
failureIndicator?.isVisible = when (informationData.sendState) { failureIndicator?.isVisible = informationData.sendState.hasFailed()
SendState.UNDELIVERED,
SendState.FAILED_UNKNOWN_DEVICES -> true
else -> false
}
} }


abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) { abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) {

View File

@ -53,10 +53,6 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
holder.mediaContentView.setOnLongClickListener(longClickListener) holder.mediaContentView.setOnLongClickListener(longClickListener)
// The sending state color will be apply to the progress text // The sending state color will be apply to the progress text
renderSendState(holder.imageView, null, holder.failedToSendIndicator) renderSendState(holder.imageView, null, holder.failedToSendIndicator)
holder.progressLayout
if (informationData.sendState.hasFailed()) {

}
holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
} }



View File

@ -84,7 +84,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************


private fun handleSelectRoom(action: RoomListActions.SelectRoom) { private fun handleSelectRoom(action: RoomListActions.SelectRoom) {
_openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId)) _openRoomLiveData.postLiveEvent(action.roomSummary.roomId)
} }


private fun handleToggleCategory(action: RoomListActions.ToggleCategory) = setState { private fun handleToggleCategory(action: RoomListActions.ToggleCategory) = setState {

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequest
import im.vector.riotx.core.utils.DimensionUtils.dpToPx import im.vector.riotx.core.utils.DimensionUtils.dpToPx
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import timber.log.Timber import timber.log.Timber
@ -67,27 +68,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
imageView.layoutParams.height = height imageView.layoutParams.height = height
imageView.layoutParams.width = width imageView.layoutParams.width = width


val glideRequest = if (data.elementToDecrypt != null) { createGlideRequest(data, mode, imageView, width, height)
// Encrypted image
GlideApp
.with(imageView)
.load(data)
} else {
// Clear image
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val resolvedUrl = when (mode) {
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
}
//Fallback to base url
?: data.url

GlideApp
.with(imageView)
.load(resolvedUrl)
}

glideRequest
.dontAnimate() .dontAnimate()
.transform(RoundedCorners(dpToPx(8, imageView.context))) .transform(RoundedCorners(dpToPx(8, imageView.context)))
.thumbnail(0.3f) .thumbnail(0.3f)
@ -95,31 +76,11 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:


} }


fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback :((Boolean) -> Unit)? = null) { fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
val (width, height) = processSize(data, mode) val (width, height) = processSize(data, mode)


val glideRequest = if (data.elementToDecrypt != null) { createGlideRequest(data, mode, imageView, width, height)
// Encrypted image .listener(object : RequestListener<Drawable> {
GlideApp
.with(imageView)
.load(data)
} else {
// Clear image
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val resolvedUrl = when (mode) {
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
}
//Fallback to base url
?: data.url

GlideApp
.with(imageView)
.load(resolvedUrl)
}

glideRequest
.listener(object: RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, override fun onLoadFailed(e: GlideException?,
model: Any?, model: Any?,
target: Target<Drawable>?, target: Target<Drawable>?,
@ -140,7 +101,28 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
}) })
.fitCenter() .fitCenter()
.into(imageView) .into(imageView)
}


private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, width: Int, height: Int): GlideRequest<Drawable> {
return if (data.elementToDecrypt != null) {
// Encrypted image
GlideApp
.with(imageView)
.load(data)
} else {
// Clear image
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val resolvedUrl = when (mode) {
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
}
//Fallback to base url
?: data.url

GlideApp
.with(imageView)
.load(resolvedUrl)
}
} }


fun render(data: Data, imageView: BigImageView) { fun render(data: Data, imageView: BigImageView) {

View File

@ -8,7 +8,6 @@
<!-- enable window content transitions --> <!-- enable window content transitions -->
<item name="android:windowContentTransitions">true</item> <item name="android:windowContentTransitions">true</item>



<!-- specify shared element enter and exit transitions --> <!-- specify shared element enter and exit transitions -->
<item name="android:windowSharedElementEnterTransition">@transition/image_preview_transition</item> <item name="android:windowSharedElementEnterTransition">@transition/image_preview_transition</item>
<item name="android:windowSharedElementExitTransition">@transition/image_preview_transition</item> <item name="android:windowSharedElementExitTransition">@transition/image_preview_transition</item>