Merge branch 'develop' into feature/room_update
This commit is contained in:
commit
77c4355aed
|
@ -7,6 +7,7 @@ Features:
|
||||||
Improvements:
|
Improvements:
|
||||||
- UI for pending edits (#193)
|
- UI for pending edits (#193)
|
||||||
- UX image preview screen transition (#393)
|
- UX image preview screen transition (#393)
|
||||||
|
- Basic support for resending failed messages (retry/remove)
|
||||||
|
|
||||||
Other changes:
|
Other changes:
|
||||||
-
|
-
|
||||||
|
@ -15,6 +16,7 @@ Bugfix:
|
||||||
- Edited message: link confusion when (edited) appears in body (#398)
|
- Edited message: link confusion when (edited) appears in body (#398)
|
||||||
- Close detail room screen when the room is left with another client (#256)
|
- Close detail room screen when the room is left with another client (#256)
|
||||||
- Clear notification for a room left on another client
|
- Clear notification for a room left on another client
|
||||||
|
- Fix messages with empty `in_reply_to` not rendering (#447)
|
||||||
|
|
||||||
Translations:
|
Translations:
|
||||||
-
|
-
|
||||||
|
|
|
@ -20,6 +20,9 @@ import android.text.TextUtils
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||||
|
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.send.SendState
|
||||||
import im.vector.matrix.android.api.util.JsonDict
|
import im.vector.matrix.android.api.util.JsonDict
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
|
@ -81,6 +84,7 @@ data class Event(
|
||||||
|
|
||||||
var mxDecryptionResult: OlmDecryptionResult? = null
|
var mxDecryptionResult: OlmDecryptionResult? = null
|
||||||
var mCryptoError: MXCryptoError.ErrorType? = null
|
var mCryptoError: MXCryptoError.ErrorType? = null
|
||||||
|
var sendState: SendState = SendState.UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -272,6 +276,7 @@ data class Event(
|
||||||
if (redacts != other.redacts) return false
|
if (redacts != other.redacts) return false
|
||||||
if (mxDecryptionResult != other.mxDecryptionResult) return false
|
if (mxDecryptionResult != other.mxDecryptionResult) return false
|
||||||
if (mCryptoError != other.mCryptoError) return false
|
if (mCryptoError != other.mCryptoError) return false
|
||||||
|
if (sendState != other.sendState) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -289,6 +294,39 @@ data class Event(
|
||||||
result = 31 * result + (redacts?.hashCode() ?: 0)
|
result = 31 * result + (redacts?.hashCode() ?: 0)
|
||||||
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
|
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
|
||||||
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + sendState.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Event.isTextMessage(): Boolean {
|
||||||
|
if (this.getClearType() == EventType.MESSAGE) {
|
||||||
|
return getClearContent()?.toModel<MessageContent>()?.let {
|
||||||
|
when (it.type) {
|
||||||
|
MessageType.MSGTYPE_TEXT,
|
||||||
|
MessageType.MSGTYPE_EMOTE,
|
||||||
|
MessageType.MSGTYPE_NOTICE -> {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Event.isImageMessage(): Boolean {
|
||||||
|
if (this.getClearType() == EventType.MESSAGE) {
|
||||||
|
return getClearContent()?.toModel<MessageContent>()?.let {
|
||||||
|
when (it.type) {
|
||||||
|
MessageType.MSGTYPE_IMAGE -> {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
|
@ -29,5 +29,5 @@ interface MessageContent {
|
||||||
|
|
||||||
|
|
||||||
fun MessageContent?.isReply(): Boolean {
|
fun MessageContent?.isReply(): Boolean {
|
||||||
return this?.relatesTo?.inReplyTo != null
|
return this?.relatesTo?.inReplyTo?.eventId != null
|
||||||
}
|
}
|
|
@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ReplyToContent(
|
data class ReplyToContent(
|
||||||
@Json(name = "event_id") val eventId: String
|
@Json(name = "event_id") val eventId: String? = null
|
||||||
)
|
)
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.send
|
||||||
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.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
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.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,4 +66,31 @@ interface SendService {
|
||||||
*/
|
*/
|
||||||
fun redactEvent(event: Event, reason: String?): Cancelable
|
fun redactEvent(event: Event, reason: String?): Cancelable
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule this message to be resent
|
||||||
|
* @param localEcho the unsent local echo
|
||||||
|
*/
|
||||||
|
fun resendTextMessage(localEcho: TimelineEvent): Cancelable?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule this message to be resent
|
||||||
|
* @param localEcho the unsent local echo
|
||||||
|
*/
|
||||||
|
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable?
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove this failed message from the timeline
|
||||||
|
* @param localEcho the unsent local echo
|
||||||
|
*/
|
||||||
|
fun deleteFailedEcho(localEcho: TimelineEvent)
|
||||||
|
|
||||||
|
fun clearSendingQueue()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend all failed messages one by one (and keep order)
|
||||||
|
*/
|
||||||
|
fun resendAllFailedMessages()
|
||||||
|
|
||||||
}
|
}
|
|
@ -41,4 +41,8 @@ enum class SendState {
|
||||||
return this == UNDELIVERED || this == FAILED_UNKNOWN_DEVICES
|
return this == UNDELIVERED || this == FAILED_UNKNOWN_DEVICES
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isSending(): Boolean {
|
||||||
|
return this == UNSENT || this == ENCRYPTING || this == SENDING
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,9 @@ interface Timeline {
|
||||||
*/
|
*/
|
||||||
fun paginate(direction: Direction, count: Int)
|
fun paginate(direction: Direction, count: Int)
|
||||||
|
|
||||||
|
fun pendingEventCount() : Int
|
||||||
|
|
||||||
|
fun failedToDeliverEventCount() : Int
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -38,7 +38,6 @@ data class TimelineEvent(
|
||||||
val senderName: String?,
|
val senderName: String?,
|
||||||
val isUniqueDisplayName: Boolean,
|
val isUniqueDisplayName: Boolean,
|
||||||
val senderAvatar: String?,
|
val senderAvatar: String?,
|
||||||
val sendState: SendState,
|
|
||||||
val annotations: EventAnnotationsSummary? = null
|
val annotations: EventAnnotationsSummary? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,7 @@ internal object EventMapper {
|
||||||
unsignedData = ud,
|
unsignedData = ud,
|
||||||
redacts = eventEntity.redacts
|
redacts = eventEntity.redacts
|
||||||
).also {
|
).also {
|
||||||
|
it.sendState = eventEntity.sendState
|
||||||
eventEntity.decryptionResultJson?.let { json ->
|
eventEntity.decryptionResultJson?.let { json ->
|
||||||
try {
|
try {
|
||||||
it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
|
it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
|
||||||
|
|
|
@ -33,8 +33,7 @@ internal object TimelineEventMapper {
|
||||||
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
|
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
|
||||||
senderName = timelineEventEntity.senderName,
|
senderName = timelineEventEntity.senderName,
|
||||||
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
||||||
senderAvatar = timelineEventEntity.senderAvatar,
|
senderAvatar = timelineEventEntity.senderAvatar
|
||||||
sendState = timelineEventEntity.root?.sendState ?: SendState.UNKNOWN
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,9 +57,11 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.success()
|
?: return Result.success()
|
||||||
|
Timber.v("Starting upload media work with params $params")
|
||||||
|
|
||||||
if (params.lastFailureMessage != null) {
|
if (params.lastFailureMessage != null) {
|
||||||
// Transmit the error
|
// Transmit the error
|
||||||
|
Timber.v("Stop upload media work due to input failure")
|
||||||
return Result.success(inputData)
|
return Result.success(inputData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +123,11 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
||||||
|
|
||||||
val progressListener = object : ProgressRequestBody.Listener {
|
val progressListener = object : ProgressRequestBody.Listener {
|
||||||
override fun onProgress(current: Long, total: Long) {
|
override fun onProgress(current: Long, total: Long) {
|
||||||
contentUploadStateTracker.setProgress(eventId, current, total)
|
if (isStopped) {
|
||||||
|
contentUploadStateTracker.setFailure(eventId, Throwable("Cancelled"))
|
||||||
|
} else {
|
||||||
|
contentUploadStateTracker.setProgress(eventId, current, total)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,6 +172,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
||||||
encryptedFileInfo: EncryptedFileInfo?,
|
encryptedFileInfo: EncryptedFileInfo?,
|
||||||
thumbnailUrl: String?,
|
thumbnailUrl: String?,
|
||||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?): Result {
|
thumbnailEncryptedFileInfo: EncryptedFileInfo?): Result {
|
||||||
|
Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped")
|
||||||
contentUploadStateTracker.setSuccess(params.event.eventId!!)
|
contentUploadStateTracker.setSuccess(params.event.eventId!!)
|
||||||
val event = updateEvent(params.event, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
|
val event = updateEvent(params.event, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
|
||||||
val sendParams = SendEventWorker.Params(params.userId, params.roomId, event)
|
val sendParams = SendEventWorker.Params(params.userId, params.roomId, event)
|
||||||
|
@ -210,6 +217,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun MessageFileContent.update(url: String,
|
private fun MessageFileContent.update(url: String,
|
||||||
encryptedFileInfo: EncryptedFileInfo?): MessageFileContent {
|
encryptedFileInfo: EncryptedFileInfo?): MessageFileContent {
|
||||||
return copy(
|
return copy(
|
||||||
|
|
|
@ -62,9 +62,7 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
|
||||||
roomId: String,
|
roomId: String,
|
||||||
membership: Membership? = null,
|
membership: Membership? = null,
|
||||||
roomSummary: RoomSyncSummary? = null,
|
roomSummary: RoomSyncSummary? = null,
|
||||||
unreadNotifications: RoomSyncUnreadNotifications? = null,
|
unreadNotifications: RoomSyncUnreadNotifications? = null) {
|
||||||
isDirect: Boolean? = null,
|
|
||||||
directUserId: String? = null) {
|
|
||||||
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
|
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
|
@ -97,10 +95,6 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.map { it.stateKey }
|
.map { it.stateKey }
|
||||||
|
|
||||||
if (isDirect != null) {
|
|
||||||
roomSummaryEntity.isDirect = isDirect
|
|
||||||
roomSummaryEntity.directUserId = directUserId
|
|
||||||
}
|
|
||||||
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
|
||||||
|
|
|
@ -86,7 +86,7 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro
|
||||||
this.isDirect = true
|
this.isDirect = true
|
||||||
}
|
}
|
||||||
}.flatMap {
|
}.flatMap {
|
||||||
val directChats = directChatsHelper.getDirectChats()
|
val directChats = directChatsHelper.getLocalUserAccount()
|
||||||
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats))
|
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats))
|
||||||
}.flatMap {
|
}.flatMap {
|
||||||
Try.just(roomId)
|
Try.just(roomId)
|
||||||
|
|
|
@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.findWithSenderMembershipEvent
|
import im.vector.matrix.android.internal.database.query.findWithSenderMembershipEvent
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
|
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||||
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
|
||||||
|
@ -63,7 +64,7 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
|
||||||
val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId
|
val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId
|
||||||
?: "").findFirst()
|
?: "").findFirst()
|
||||||
?: return
|
?: return
|
||||||
val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT
|
val isLocalEcho = LocalEchoEventFactory.isLocalEchoId(redactionEvent.eventId ?: "")
|
||||||
Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho")
|
Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho")
|
||||||
|
|
||||||
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
|
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
|
||||||
|
|
|
@ -17,25 +17,36 @@
|
||||||
package im.vector.matrix.android.internal.session.room.send
|
package im.vector.matrix.android.internal.session.room.send
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.*
|
||||||
import androidx.work.ExistingWorkPolicy
|
|
||||||
import androidx.work.OneTimeWorkRequest
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.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.Event
|
||||||
|
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.MessageType
|
||||||
import im.vector.matrix.android.api.session.room.send.SendService
|
import im.vector.matrix.android.api.session.room.send.SendService
|
||||||
|
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.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
import im.vector.matrix.android.api.util.CancelableBag
|
import im.vector.matrix.android.api.util.CancelableBag
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
|
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.util.tryTransactionAsync
|
||||||
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 timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -50,6 +61,7 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
|
||||||
private val monarchy: Monarchy)
|
private val monarchy: Monarchy)
|
||||||
: SendService {
|
: SendService {
|
||||||
|
|
||||||
|
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
|
||||||
override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable {
|
override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable {
|
||||||
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
|
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
|
||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
|
@ -70,7 +82,7 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
|
||||||
// Encrypted room handling
|
// Encrypted room handling
|
||||||
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)
|
val encryptWork = createEncryptEventWork(event, true)
|
||||||
val sendWork = createSendEventWork(event)
|
val sendWork = createSendEventWork(event)
|
||||||
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, sendWork)
|
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, sendWork)
|
||||||
CancelableWork(context, encryptWork.id)
|
CancelableWork(context, encryptWork.id)
|
||||||
|
@ -94,25 +106,162 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
|
||||||
return CancelableWork(context, redactWork.id)
|
return CancelableWork(context, redactWork.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
|
||||||
|
if (localEcho.root.isTextMessage()) {
|
||||||
|
return sendEvent(localEcho.root)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? {
|
||||||
|
//TODO this need a refactoring of attachement sending
|
||||||
|
// val clearContent = localEcho.root.getClearContent()
|
||||||
|
// val messageContent = clearContent?.toModel<MessageContent>() ?: return null
|
||||||
|
// when (messageContent.type) {
|
||||||
|
// MessageType.MSGTYPE_IMAGE -> {
|
||||||
|
// val imageContent = clearContent.toModel<MessageImageContent>() ?: return null
|
||||||
|
// val url = imageContent.url ?: return null
|
||||||
|
// if (url.startsWith("mxc://")) {
|
||||||
|
// //TODO
|
||||||
|
// } else {
|
||||||
|
// //The image has not yet been sent
|
||||||
|
// val attachmentData = ContentAttachmentData(
|
||||||
|
// size = imageContent.info!!.size.toLong(),
|
||||||
|
// mimeType = imageContent.info.mimeType!!,
|
||||||
|
// width = imageContent.info.width.toLong(),
|
||||||
|
// height = imageContent.info.height.toLong(),
|
||||||
|
// name = imageContent.body,
|
||||||
|
// path = imageContent.url,
|
||||||
|
// type = ContentAttachmentData.Type.IMAGE
|
||||||
|
// )
|
||||||
|
// monarchy.runTransactionSync {
|
||||||
|
// EventEntity.where(it,eventId = localEcho.root.eventId ?: "").findFirst()?.let {
|
||||||
|
// it.sendState = SendState.UNSENT
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return internalSendMedia(localEcho.root,attachmentData)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return null
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteFailedEcho(localEcho: TimelineEvent) {
|
||||||
|
monarchy.tryTransactionAsync { realm ->
|
||||||
|
TimelineEventEntity.where(realm, eventId = localEcho.root.eventId
|
||||||
|
?: "").findFirst()?.let {
|
||||||
|
it.deleteFromRealm()
|
||||||
|
}
|
||||||
|
EventEntity.where(realm, eventId = localEcho.root.eventId
|
||||||
|
?: "").findFirst()?.let {
|
||||||
|
it.deleteFromRealm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearSendingQueue() {
|
||||||
|
TimelineSendEventWorkCommon.cancelAllWorks(context, roomId)
|
||||||
|
WorkManager.getInstance(context).cancelUniqueWork(buildWorkIdentifier(UPLOAD_WORK))
|
||||||
|
|
||||||
|
matrixOneTimeWorkRequestBuilder<FakeSendWorker>()
|
||||||
|
.build().let {
|
||||||
|
TimelineSendEventWorkCommon.postWork(context, roomId, it, ExistingWorkPolicy.REPLACE)
|
||||||
|
|
||||||
|
//need to clear also image sending queue
|
||||||
|
WorkManager.getInstance(context)
|
||||||
|
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it)
|
||||||
|
.enqueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
monarchy.tryTransactionAsync { realm ->
|
||||||
|
RoomEntity.where(realm, roomId).findFirst()?.let { room ->
|
||||||
|
room.sendingTimelineEvents.forEach {
|
||||||
|
it.root?.sendState = SendState.UNDELIVERED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resendAllFailedMessages() {
|
||||||
|
monarchy.tryTransactionAsync { realm ->
|
||||||
|
RoomEntity.where(realm, roomId).findFirst()?.let { room ->
|
||||||
|
room.sendingTimelineEvents.filter {
|
||||||
|
it.root?.sendState?.hasFailed() ?: false
|
||||||
|
}.sortedBy { it.root?.originServerTs ?: 0 }.forEach { timelineEventEntity ->
|
||||||
|
timelineEventEntity.root?.let {
|
||||||
|
val event = it.asDomain()
|
||||||
|
when (event.getClearType()) {
|
||||||
|
EventType.MESSAGE,
|
||||||
|
EventType.REDACTION,
|
||||||
|
EventType.REACTION -> {
|
||||||
|
val content = event.getClearContent().toModel<MessageContent>()
|
||||||
|
if (content != null) {
|
||||||
|
when (content.type) {
|
||||||
|
MessageType.MSGTYPE_EMOTE,
|
||||||
|
MessageType.MSGTYPE_NOTICE,
|
||||||
|
MessageType.MSGTYPE_LOCATION,
|
||||||
|
MessageType.MSGTYPE_TEXT -> {
|
||||||
|
it.sendState = SendState.UNSENT
|
||||||
|
sendEvent(event)
|
||||||
|
}
|
||||||
|
MessageType.MSGTYPE_FILE,
|
||||||
|
MessageType.MSGTYPE_VIDEO,
|
||||||
|
MessageType.MSGTYPE_IMAGE,
|
||||||
|
MessageType.MSGTYPE_AUDIO -> {
|
||||||
|
//need to resend the attachement
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Timber.e("Cannot resend message ${event.type} / ${content.type}")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.e("Unsupported message to resend ${event.type}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Timber.e("Unsupported message to resend ${event.type}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
|
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
|
||||||
// Create an event with the media file path
|
// Create an event with the media file path
|
||||||
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
|
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
|
||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return internalSendMedia(event, attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): CancelableWork {
|
||||||
val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId)
|
val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId)
|
||||||
|
|
||||||
val uploadWork = createUploadMediaWork(event, attachment, isRoomEncrypted)
|
val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true)
|
||||||
val sendWork = createSendEventWork(event)
|
val sendWork = createSendEventWork(localEcho)
|
||||||
|
|
||||||
if (isRoomEncrypted) {
|
if (isRoomEncrypted) {
|
||||||
val encryptWork = createEncryptEventWork(event)
|
val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/)
|
||||||
|
|
||||||
WorkManager.getInstance(context)
|
val op: Operation = WorkManager.getInstance(context)
|
||||||
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||||
.then(encryptWork)
|
.then(encryptWork)
|
||||||
.then(sendWork)
|
.then(sendWork)
|
||||||
.enqueue()
|
.enqueue()
|
||||||
|
op.result.addListener(Runnable {
|
||||||
|
if (op.result.isCancelled) {
|
||||||
|
Timber.e("CHAINE WAS CANCELLED")
|
||||||
|
} else if (op.state.value is Operation.State.FAILURE) {
|
||||||
|
Timber.e("CHAINE DID FAIL")
|
||||||
|
}
|
||||||
|
}, workerFutureListenerExecutor)
|
||||||
} else {
|
} else {
|
||||||
WorkManager.getInstance(context)
|
WorkManager.getInstance(context)
|
||||||
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||||
|
@ -131,7 +280,7 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
|
||||||
return "${roomId}_$identifier"
|
return "${roomId}_$identifier"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createEncryptEventWork(event: Event): OneTimeWorkRequest {
|
private fun createEncryptEventWork(event: Event, startChain: Boolean = false): 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)
|
||||||
|
@ -139,6 +288,11 @@ 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 {
|
||||||
|
if (startChain) {
|
||||||
|
setInputMerger(NoMerger::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -159,15 +313,24 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
|
||||||
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
|
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData, isRoomEncrypted: Boolean): OneTimeWorkRequest {
|
private fun createUploadMediaWork(event: Event,
|
||||||
|
attachment: ContentAttachmentData,
|
||||||
|
isRoomEncrypted: Boolean,
|
||||||
|
startChain: Boolean = false): 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 {
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResul
|
||||||
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -49,10 +50,13 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||||
@Inject lateinit var localEchoUpdater: LocalEchoUpdater
|
@Inject lateinit var localEchoUpdater: LocalEchoUpdater
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
|
Timber.v("Start Encrypt work")
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.success()
|
?: return Result.success().also {
|
||||||
|
Timber.v("Work cancelled due to input error from parent")
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.v("Start Encrypt work for event ${params.event.eventId}")
|
||||||
if (params.lastFailureMessage != null) {
|
if (params.lastFailureMessage != null) {
|
||||||
// Transmit the error
|
// Transmit the error
|
||||||
return Result.success(inputData)
|
return Result.success(inputData)
|
||||||
|
@ -97,7 +101,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||||
latch.await()
|
latch.await()
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
var modifiedContent = HashMap(result?.eventContent)
|
val modifiedContent = HashMap(result?.eventContent)
|
||||||
params.keepKeys?.forEach { toKeep ->
|
params.keepKeys?.forEach { toKeep ->
|
||||||
localEvent.content?.get(toKeep)?.let {
|
localEvent.content?.get(toKeep)?.let {
|
||||||
//put it back in the encrypted thing
|
//put it back in the encrypted thing
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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.send
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
|
||||||
|
internal class FakeSendWorker(context: Context, params: WorkerParameters)
|
||||||
|
: Worker(context, params) {
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,15 +21,21 @@ import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
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.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class LocalEchoUpdater @Inject constructor(private val monarchy: Monarchy) {
|
internal class LocalEchoUpdater @Inject constructor(private val monarchy: Monarchy) {
|
||||||
|
|
||||||
fun updateSendState(eventId: String, sendState: SendState) {
|
fun updateSendState(eventId: String, sendState: SendState) {
|
||||||
|
Timber.v("Update local state of $eventId to ${sendState.name}")
|
||||||
monarchy.tryTransactionAsync { realm ->
|
monarchy.tryTransactionAsync { realm ->
|
||||||
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
|
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
|
||||||
if (sendingEventEntity != null) {
|
if (sendingEventEntity != null) {
|
||||||
sendingEventEntity.sendState = sendState
|
if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) {
|
||||||
|
//If already synced, do not put as sent
|
||||||
|
} else {
|
||||||
|
sendingEventEntity.sendState = sendState
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.internal.session.room.send
|
||||||
|
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.InputMerger
|
||||||
|
|
||||||
|
class NoMerger : InputMerger() {
|
||||||
|
override fun merge(inputs: MutableList<Data>): Data {
|
||||||
|
return inputs.first()
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,7 +82,10 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
|
||||||
Result.success()
|
Result.success()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, { Result.success() })
|
}, {
|
||||||
|
localEchoUpdater.updateSendState(event.eventId, SendState.SENT)
|
||||||
|
Result.success()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,6 +206,23 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun pendingEventCount(): Int {
|
||||||
|
var count = 0
|
||||||
|
Realm.getInstance(realmConfiguration).use {
|
||||||
|
count = RoomEntity.where(it,roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun failedToDeliverEventCount(): Int {
|
||||||
|
var count = 0
|
||||||
|
Realm.getInstance(realmConfiguration).use {
|
||||||
|
count = RoomEntity.where(it,roomId).findFirst()?.sendingTimelineEvents?.filter {
|
||||||
|
it.root?.sendState?.hasFailed() ?: false
|
||||||
|
}?.count() ?: 0
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
if (isStarted.compareAndSet(false, true)) {
|
if (isStarted.compareAndSet(false, true)) {
|
||||||
|
|
|
@ -51,9 +51,9 @@ internal object TimelineSendEventWorkCommon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postWork(context: Context, roomId: String, workRequest: OneTimeWorkRequest) {
|
fun postWork(context: Context, roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND) {
|
||||||
WorkManager.getInstance(context)
|
WorkManager.getInstance(context)
|
||||||
.beginUniqueWork(buildWorkIdentifier(roomId), ExistingWorkPolicy.APPEND, workRequest)
|
.beginUniqueWork(buildWorkIdentifier(roomId), policy, workRequest)
|
||||||
.enqueue()
|
.enqueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,4 +68,8 @@ internal object TimelineSendEventWorkCommon {
|
||||||
private fun buildWorkIdentifier(roomId: String): String {
|
private fun buildWorkIdentifier(roomId: String): String {
|
||||||
return "${roomId}_$SEND_WORK"
|
return "${roomId}_$SEND_WORK"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cancelAllWorks(context: Context, roomId: String) {
|
||||||
|
WorkManager.getInstance(context).cancelUniqueWork(buildWorkIdentifier(roomId))
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -35,14 +35,10 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
|
||||||
import im.vector.matrix.android.internal.database.query.find
|
import im.vector.matrix.android.internal.database.query.find
|
||||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
import im.vector.matrix.android.internal.database.query.isDirect
|
|
||||||
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.DefaultInitialSyncProgressService
|
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
|
||||||
import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
|
|
||||||
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
|
|
||||||
import im.vector.matrix.android.internal.session.mapWithProgress
|
import im.vector.matrix.android.internal.session.mapWithProgress
|
||||||
import im.vector.matrix.android.internal.session.notification.DefaultPushRuleService
|
import im.vector.matrix.android.internal.session.notification.DefaultPushRuleService
|
||||||
import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
|
import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
|
||||||
|
@ -70,9 +66,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
private val tokenStore: SyncTokenStore,
|
private val tokenStore: SyncTokenStore,
|
||||||
private val pushRuleService: DefaultPushRuleService,
|
private val pushRuleService: DefaultPushRuleService,
|
||||||
private val processForPushTask: ProcessEventForPushTask,
|
private val processForPushTask: ProcessEventForPushTask,
|
||||||
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
|
|
||||||
private val credentials: Credentials,
|
private val credentials: Credentials,
|
||||||
private val directChatsHelper: DirectChatsHelper,
|
|
||||||
private val taskExecutor: TaskExecutor) {
|
private val taskExecutor: TaskExecutor) {
|
||||||
|
|
||||||
sealed class HandlingStrategy {
|
sealed class HandlingStrategy {
|
||||||
|
@ -192,21 +186,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
|
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
|
||||||
roomEntity.addOrUpdate(chunkEntity)
|
roomEntity.addOrUpdate(chunkEntity)
|
||||||
}
|
}
|
||||||
val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(credentials.userId)
|
roomSummaryUpdater.update(realm, roomId, Membership.INVITE)
|
||||||
val inviterId = myUserStateEvent?.sender
|
|
||||||
val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() }
|
|
||||||
val isDirect = myUserRoomMember?.isDirect
|
|
||||||
if (isDirect == true && inviterId != null) {
|
|
||||||
val isAlreadyDirect = RoomSummaryEntity.isDirect(realm, roomId)
|
|
||||||
if (!isAlreadyDirect) {
|
|
||||||
val directChatsMap = directChatsHelper.getDirectChats(include = Pair(inviterId, roomId))
|
|
||||||
val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams(
|
|
||||||
directMessages = directChatsMap
|
|
||||||
)
|
|
||||||
updateUserAccountDataTask.configureWith(updateUserAccountParams).executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
roomSummaryUpdater.update(realm, roomId, Membership.INVITE, isDirect = isDirect, directUserId = inviterId)
|
|
||||||
return roomEntity
|
return roomEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,23 @@
|
||||||
package im.vector.matrix.android.internal.session.sync
|
package im.vector.matrix.android.internal.session.sync
|
||||||
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.R
|
import im.vector.matrix.android.R
|
||||||
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
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.crypto.CryptoManager
|
import im.vector.matrix.android.internal.crypto.CryptoManager
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.isDirect
|
||||||
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
|
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
|
||||||
import im.vector.matrix.android.internal.session.reportSubtask
|
import im.vector.matrix.android.internal.session.reportSubtask
|
||||||
|
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||||
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
||||||
|
import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
|
||||||
|
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
|
||||||
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
@ -88,9 +100,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
|
||||||
measureTimeMillis {
|
measureTimeMillis {
|
||||||
reportSubtask(reporter, R.string.initial_sync_start_importing_account_data, 100, 0.1f) {
|
reportSubtask(reporter, R.string.initial_sync_start_importing_account_data, 100, 0.1f) {
|
||||||
Timber.v("Handle accountData")
|
Timber.v("Handle accountData")
|
||||||
if (syncResponse.accountData != null) {
|
userAccountDataSyncHandler.handle(syncResponse.accountData, syncResponse.rooms?.invite)
|
||||||
userAccountDataSyncHandler.handle(syncResponse.accountData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.also {
|
}.also {
|
||||||
Timber.v("Finish handling accountData in $it ms")
|
Timber.v("Finish handling accountData in $it ms")
|
||||||
|
@ -98,7 +108,6 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
|
||||||
|
|
||||||
Timber.v("On sync completed")
|
Timber.v("On sync completed")
|
||||||
cryptoSyncHandler.onSyncCompleted(syncResponse)
|
cryptoSyncHandler.onSyncCompleted(syncResponse)
|
||||||
|
|
||||||
}
|
}
|
||||||
Timber.v("Finish handling sync in $measure ms")
|
Timber.v("Finish handling sync in $measure ms")
|
||||||
syncResponse
|
syncResponse
|
||||||
|
|
|
@ -17,28 +17,45 @@
|
||||||
package im.vector.matrix.android.internal.session.sync
|
package im.vector.matrix.android.internal.session.sync
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
|
|
||||||
import im.vector.matrix.android.internal.database.query.getDirectRooms
|
import im.vector.matrix.android.internal.database.query.getDirectRooms
|
||||||
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.room.membership.RoomMembers
|
||||||
|
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
|
||||||
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirectMessages
|
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirectMessages
|
||||||
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataSync
|
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataSync
|
||||||
|
import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
|
||||||
|
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
|
||||||
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
|
import io.realm.Realm
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class UserAccountDataSyncHandler @Inject constructor(private val monarchy: Monarchy) {
|
internal class UserAccountDataSyncHandler @Inject constructor(private val monarchy: Monarchy,
|
||||||
|
private val credentials: Credentials,
|
||||||
|
private val directChatsHelper: DirectChatsHelper,
|
||||||
|
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
|
||||||
|
private val taskExecutor: TaskExecutor) {
|
||||||
|
|
||||||
fun handle(accountData: UserAccountDataSync) {
|
fun handle(accountData: UserAccountDataSync?, invites: Map<String, InvitedRoomSync>?) {
|
||||||
accountData.list.forEach {
|
accountData?.list?.forEach {
|
||||||
when (it) {
|
when (it) {
|
||||||
is UserAccountDataDirectMessages -> handleDirectChatRooms(it)
|
is UserAccountDataDirectMessages -> handleDirectChatRooms(it)
|
||||||
else -> return@forEach
|
else -> return@forEach
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
monarchy.doWithRealm { realm ->
|
||||||
|
synchronizeWithServerIfNeeded(realm, invites)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDirectChatRooms(directMessages: UserAccountDataDirectMessages) {
|
private fun handleDirectChatRooms(directMessages: UserAccountDataDirectMessages) {
|
||||||
monarchy.runTransactionSync { realm ->
|
monarchy.runTransactionSync { realm ->
|
||||||
|
|
||||||
val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
|
val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
|
||||||
oldDirectRooms.forEach {
|
oldDirectRooms.forEach {
|
||||||
it.isDirect = false
|
it.isDirect = false
|
||||||
|
@ -57,4 +74,35 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we get some direct chat invites, we synchronize the user account data including those.
|
||||||
|
private fun synchronizeWithServerIfNeeded(realm: Realm, invites: Map<String, InvitedRoomSync>?) {
|
||||||
|
if (invites.isNullOrEmpty()) return
|
||||||
|
val directChats = directChatsHelper.getLocalUserAccount()
|
||||||
|
var hasUpdate = false
|
||||||
|
invites.forEach { (roomId, _) ->
|
||||||
|
val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(credentials.userId)
|
||||||
|
val inviterId = myUserStateEvent?.sender
|
||||||
|
val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() }
|
||||||
|
val isDirect = myUserRoomMember?.isDirect
|
||||||
|
if (inviterId != null && inviterId != credentials.userId && isDirect == true) {
|
||||||
|
directChats
|
||||||
|
.getOrPut(inviterId, { arrayListOf() })
|
||||||
|
.apply {
|
||||||
|
if (contains(roomId)) {
|
||||||
|
Timber.v("Direct chats already include room $roomId with user $inviterId")
|
||||||
|
} else {
|
||||||
|
add(roomId)
|
||||||
|
hasUpdate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasUpdate) {
|
||||||
|
val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams(
|
||||||
|
directMessages = directChats
|
||||||
|
)
|
||||||
|
updateUserAccountDataTask.configureWith(updateUserAccountParams).executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -24,27 +24,24 @@ import io.realm.RealmConfiguration
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DirectChatsHelper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) {
|
internal class DirectChatsHelper @Inject constructor(@SessionDatabase
|
||||||
|
private val realmConfiguration: RealmConfiguration) {
|
||||||
|
|
||||||
fun getDirectChats(include: Pair<String, String>? = null, filterRoomId: String? = null): Map<String, List<String>> {
|
/**
|
||||||
|
* @return a map of userId <-> list of roomId
|
||||||
|
*/
|
||||||
|
fun getLocalUserAccount(filterRoomId: String? = null): MutableMap<String, MutableList<String>> {
|
||||||
return Realm.getInstance(realmConfiguration).use { realm ->
|
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
|
val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
|
||||||
val directChatsMap = mutableMapOf<String, MutableList<String>>()
|
val directChatsMap = mutableMapOf<String, MutableList<String>>()
|
||||||
for (directRoom in currentDirectRooms) {
|
for (directRoom in currentDirectRooms) {
|
||||||
if (directRoom.roomId == filterRoomId) continue
|
if (directRoom.roomId == filterRoomId) continue
|
||||||
val directUserId = directRoom.directUserId ?: continue
|
val directUserId = directRoom.directUserId ?: continue
|
||||||
directChatsMap.getOrPut(directUserId, { arrayListOf() }).apply {
|
directChatsMap
|
||||||
add(directRoom.roomId)
|
.getOrPut(directUserId, { arrayListOf() })
|
||||||
}
|
.apply {
|
||||||
}
|
add(directRoom.roomId)
|
||||||
if (include != null) {
|
}
|
||||||
directChatsMap.getOrPut(include.first, { arrayListOf() }).apply {
|
|
||||||
if (contains(include.second)) {
|
|
||||||
Timber.v("Direct chats already include room ${include.second} with user ${include.first}")
|
|
||||||
} else {
|
|
||||||
add(include.second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
directChatsMap
|
directChatsMap
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<string name="event_status_sending_message">Sending message…</string>
|
||||||
|
<string name="clear_timeline_send_queue">Clear sending queue</string>
|
||||||
</resources>
|
</resources>
|
|
@ -21,5 +21,5 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
fun TimelineEvent.canReact(): Boolean {
|
fun TimelineEvent.canReact(): Boolean {
|
||||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||||
return root.getClearType() == EventType.MESSAGE && sendState.isSent() && !root.isRedacted()
|
return root.getClearType() == EventType.MESSAGE && root.sendState.isSent() && !root.isRedacted()
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,10 @@ sealed class RoomDetailActions {
|
||||||
data class EnterEditMode(val eventId: String) : RoomDetailActions()
|
data class EnterEditMode(val eventId: String) : RoomDetailActions()
|
||||||
data class EnterQuoteMode(val eventId: String) : RoomDetailActions()
|
data class EnterQuoteMode(val eventId: String) : RoomDetailActions()
|
||||||
data class EnterReplyMode(val eventId: String) : RoomDetailActions()
|
data class EnterReplyMode(val eventId: String) : RoomDetailActions()
|
||||||
|
data class ResendMessage(val eventId: String) : RoomDetailActions()
|
||||||
|
data class RemoveFailedEcho(val eventId: String) : RoomDetailActions()
|
||||||
|
object ClearSendQueue : RoomDetailActions()
|
||||||
|
object ResendAll : RoomDetailActions()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -19,7 +19,12 @@ package im.vector.riotx.features.home.room.detail
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import com.airbnb.mvrx.activityViewModel
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
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.ToolbarConfigurable
|
||||||
|
|
|
@ -27,9 +27,7 @@ import android.os.Parcelable
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.*
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
@ -38,6 +36,7 @@ import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.forEach
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
@ -188,6 +187,8 @@ class RoomDetailFragment :
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_room_detail
|
override fun getLayoutResId() = R.layout.fragment_room_detail
|
||||||
|
|
||||||
|
override fun getMenuRes() = R.menu.menu_timeline
|
||||||
|
|
||||||
private lateinit var actionViewModel: ActionsHandler
|
private lateinit var actionViewModel: ActionsHandler
|
||||||
|
|
||||||
@BindView(R.id.composerLayout)
|
@BindView(R.id.composerLayout)
|
||||||
|
@ -271,6 +272,27 @@ class RoomDetailFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
|
menu.forEach {
|
||||||
|
it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
if (item.itemId == R.id.clear_message_queue) {
|
||||||
|
//This a temporary option during dev as it is not super stable
|
||||||
|
//Cancel all pending actions in room queue and post a dummy
|
||||||
|
//Then mark all sending events as undelivered
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.ClearSendQueue)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (item.itemId == R.id.resend_all) {
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.ResendAll)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
private fun exitSpecialMode() {
|
private fun exitSpecialMode() {
|
||||||
commandAutocompletePolicy.enabled = true
|
commandAutocompletePolicy.enabled = true
|
||||||
composerLayout.collapse()
|
composerLayout.collapse()
|
||||||
|
@ -874,6 +896,14 @@ class RoomDetailFragment :
|
||||||
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 -> {
|
||||||
|
val eventId = actionData.data.toString()
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.ResendMessage(eventId))
|
||||||
|
}
|
||||||
|
MessageMenuViewModel.ACTION_REMOVE -> {
|
||||||
|
val eventId = actionData.data.toString()
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(eventId))
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show()
|
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
import androidx.annotation.IdRes
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
|
@ -32,6 +33,8 @@ import im.vector.matrix.android.api.MatrixPatterns
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
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.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
||||||
|
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.file.FileService
|
import im.vector.matrix.android.api.session.file.FileService
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
|
@ -43,6 +46,7 @@ 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.android.internal.crypto.model.event.EncryptedEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.postLiveEvent
|
import im.vector.riotx.core.extensions.postLiveEvent
|
||||||
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
|
||||||
|
@ -123,6 +127,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
is RoomDetailActions.DownloadFile -> handleDownloadFile(action)
|
is RoomDetailActions.DownloadFile -> handleDownloadFile(action)
|
||||||
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
|
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
|
||||||
is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||||
|
is RoomDetailActions.ResendMessage -> handleResendEvent(action)
|
||||||
|
is RoomDetailActions.RemoveFailedEcho -> handleRemove(action)
|
||||||
|
is RoomDetailActions.ClearSendQueue -> handleClearSendQueue()
|
||||||
|
is RoomDetailActions.ResendAll -> handleResendAll()
|
||||||
else -> Timber.e("Unhandled Action: $action")
|
else -> Timber.e("Unhandled Action: $action")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -186,6 +194,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
get() = _downloadedFileEvent
|
get() = _downloadedFileEvent
|
||||||
|
|
||||||
|
|
||||||
|
fun isMenuItemVisible(@IdRes itemId: Int): Boolean {
|
||||||
|
if (itemId == R.id.clear_message_queue) {
|
||||||
|
//For now always disable, woker cancellation is not working properly
|
||||||
|
return false//timeline.pendingEventCount() > 0
|
||||||
|
}
|
||||||
|
if (itemId == R.id.resend_all) {
|
||||||
|
return timeline.failedToDeliverEventCount() > 0
|
||||||
|
}
|
||||||
|
if (itemId == R.id.clear_all) {
|
||||||
|
return timeline.failedToDeliverEventCount() > 0
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
|
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
|
||||||
|
@ -419,7 +441,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
|
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
|
||||||
if (action.event.sendState.isSent()) { //ignore pending/local events
|
if (action.event.root.sendState.isSent()) { //ignore pending/local events
|
||||||
displayedEventsObservable.accept(action)
|
displayedEventsObservable.accept(action)
|
||||||
}
|
}
|
||||||
//We need to update this with the related m.replace also (to move read receipt)
|
//We need to update this with the related m.replace also (to move read receipt)
|
||||||
|
@ -553,6 +575,46 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleResendEvent(action: RoomDetailActions.ResendMessage) {
|
||||||
|
val targetEventId = action.eventId
|
||||||
|
room.getTimeLineEvent(targetEventId)?.let {
|
||||||
|
//State must be UNDELIVERED or Failed
|
||||||
|
if (!it.root.sendState.hasFailed()) {
|
||||||
|
Timber.e("Cannot resend message, it is not failed, Cancel first")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (it.root.isTextMessage()) {
|
||||||
|
room.resendTextMessage(it)
|
||||||
|
} else if (it.root.isImageMessage()) {
|
||||||
|
room.resendMediaMessage(it)
|
||||||
|
} else {
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRemove(action: RoomDetailActions.RemoveFailedEcho) {
|
||||||
|
val targetEventId = action.eventId
|
||||||
|
room.getTimeLineEvent(targetEventId)?.let {
|
||||||
|
//State must be UNDELIVERED or Failed
|
||||||
|
if (!it.root.sendState.hasFailed()) {
|
||||||
|
Timber.e("Cannot resend message, it is not failed, Cancel first")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
room.deleteFailedEcho(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleClearSendQueue() {
|
||||||
|
room.clearSendingQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleResendAll() {
|
||||||
|
room.resendAllFailedMessages()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun observeEventDisplayedActions() {
|
private fun observeEventDisplayedActions() {
|
||||||
// We are buffering scroll events for one second
|
// We are buffering scroll events for one second
|
||||||
// and keep the most recent one to set the read receipt on.
|
// and keep the most recent one to set the read receipt on.
|
||||||
|
|
|
@ -130,8 +130,7 @@ class RoomMessageTouchHelperCallback(private val context: Context,
|
||||||
|
|
||||||
|
|
||||||
private fun drawReplyButton(canvas: Canvas, itemView: View) {
|
private fun drawReplyButton(canvas: Canvas, itemView: View) {
|
||||||
|
//Timber.v("drawReplyButton")
|
||||||
Timber.v("drawReplyButton")
|
|
||||||
val translationX = Math.abs(itemView.translationX)
|
val translationX = Math.abs(itemView.translationX)
|
||||||
val newTime = System.currentTimeMillis()
|
val newTime = System.currentTimeMillis()
|
||||||
val dt = Math.min(17, newTime - lastReplyButtonAnimationTime)
|
val dt = Math.min(17, newTime - lastReplyButtonAnimationTime)
|
||||||
|
|
|
@ -138,6 +138,19 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
quickReactBottomDivider.isVisible = it.canReact()
|
quickReactBottomDivider.isVisible = it.canReact()
|
||||||
bottom_sheet_quick_reaction_container.isVisible = it.canReact()
|
bottom_sheet_quick_reaction_container.isVisible = it.canReact()
|
||||||
|
if (it.informationData.sendState.isSending()) {
|
||||||
|
messageStatusInfo.isVisible = true
|
||||||
|
messageStatusProgress.isVisible = true
|
||||||
|
messageStatusText.text = getString(R.string.event_status_sending_message)
|
||||||
|
messageStatusText.setCompoundDrawables(null, null, null, null)
|
||||||
|
} else if (it.informationData.sendState.hasFailed()) {
|
||||||
|
messageStatusInfo.isVisible = true
|
||||||
|
messageStatusProgress.isVisible = false
|
||||||
|
messageStatusText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_warning_small, 0, 0, 0)
|
||||||
|
messageStatusText.text = getString(R.string.unable_to_send_message)
|
||||||
|
} else {
|
||||||
|
messageStatusInfo.isVisible = false
|
||||||
|
}
|
||||||
return@withState
|
return@withState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
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.EventType
|
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.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.MessageImageContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||||
|
@ -75,7 +76,9 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
|
||||||
const val ACTION_REPLY = "reply"
|
const val ACTION_REPLY = "reply"
|
||||||
const val ACTION_SHARE = "share"
|
const val ACTION_SHARE = "share"
|
||||||
const val ACTION_RESEND = "resend"
|
const val ACTION_RESEND = "resend"
|
||||||
|
const val ACTION_REMOVE = "remove"
|
||||||
const val ACTION_DELETE = "delete"
|
const val ACTION_DELETE = "delete"
|
||||||
|
const val ACTION_CANCEL = "cancel"
|
||||||
const val VIEW_SOURCE = "VIEW_SOURCE"
|
const val VIEW_SOURCE = "VIEW_SOURCE"
|
||||||
const val VIEW_DECRYPTED_SOURCE = "VIEW_DECRYPTED_SOURCE"
|
const val VIEW_DECRYPTED_SOURCE = "VIEW_DECRYPTED_SOURCE"
|
||||||
const val ACTION_COPY_PERMALINK = "ACTION_COPY_PERMALINK"
|
const val ACTION_COPY_PERMALINK = "ACTION_COPY_PERMALINK"
|
||||||
|
@ -110,56 +113,57 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
|
||||||
?: event.root.getClearContent().toModel()
|
?: event.root.getClearContent().toModel()
|
||||||
val type = messageContent?.type
|
val type = messageContent?.type
|
||||||
|
|
||||||
val actions = if (!event.sendState.isSent()) {
|
return if (event.root.sendState.hasFailed()) {
|
||||||
//Resend and Delete
|
arrayListOf<SimpleAction>().apply {
|
||||||
listOf<SimpleAction>(
|
if (canRetry(event)) {
|
||||||
// SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_send, event.root.eventId),
|
this.add(SimpleAction(ACTION_RESEND, R.string.global_retry, R.drawable.ic_refresh_cw, eventId))
|
||||||
// //TODO delete icon
|
}
|
||||||
// SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId)
|
this.add(SimpleAction(ACTION_REMOVE, R.string.remove, R.drawable.ic_trash, eventId))
|
||||||
)
|
}
|
||||||
|
} else if (event.root.sendState.isSending()) {
|
||||||
|
//TODO is uploading attachment?
|
||||||
|
arrayListOf<SimpleAction>().apply {
|
||||||
|
if (canCancel(event)) {
|
||||||
|
this.add(SimpleAction(ACTION_CANCEL, R.string.cancel, R.drawable.ic_close_round, eventId))
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
arrayListOf<SimpleAction>().apply {
|
arrayListOf<SimpleAction>().apply {
|
||||||
|
|
||||||
if (event.sendState == SendState.SENDING) {
|
|
||||||
//TODO add cancel?
|
|
||||||
return@apply
|
|
||||||
}
|
|
||||||
//TODO is downloading attachement?
|
|
||||||
|
|
||||||
if (!event.root.isRedacted()) {
|
if (!event.root.isRedacted()) {
|
||||||
|
|
||||||
if (canReply(event, messageContent)) {
|
if (canReply(event, messageContent)) {
|
||||||
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId))
|
add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canEdit(event, session.myUserId)) {
|
if (canEdit(event, session.myUserId)) {
|
||||||
this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, eventId))
|
add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, eventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canRedact(event, session.myUserId)) {
|
if (canRedact(event, session.myUserId)) {
|
||||||
this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, eventId))
|
add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, eventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canCopy(type)) {
|
if (canCopy(type)) {
|
||||||
//TODO copy images? html? see ClipBoard
|
//TODO copy images? html? see ClipBoard
|
||||||
this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent!!.body))
|
add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent!!.body))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.canReact()) {
|
if (event.canReact()) {
|
||||||
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, eventId))
|
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))
|
add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canViewReactions(event)) {
|
if (canViewReactions(event)) {
|
||||||
this.add(SimpleAction(ACTION_VIEW_REACTIONS, R.string.message_view_reaction, R.drawable.ic_view_reactions, informationData))
|
add(SimpleAction(ACTION_VIEW_REACTIONS, R.string.message_view_reaction, R.drawable.ic_view_reactions, informationData))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canShare(type)) {
|
if (canShare(type)) {
|
||||||
if (messageContent is MessageImageContent) {
|
if (messageContent is MessageImageContent) {
|
||||||
this.add(
|
add(
|
||||||
SimpleAction(ACTION_SHARE,
|
SimpleAction(ACTION_SHARE,
|
||||||
R.string.share, R.drawable.ic_share,
|
R.string.share, R.drawable.ic_share,
|
||||||
session.contentUrlResolver().resolveFullSize(messageContent.url))
|
session.contentUrlResolver().resolveFullSize(messageContent.url))
|
||||||
|
@ -169,7 +173,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (event.sendState == SendState.SENT) {
|
if (event.root.sendState == SendState.SENT) {
|
||||||
|
|
||||||
//TODO Can be redacted
|
//TODO Can be redacted
|
||||||
|
|
||||||
|
@ -177,23 +181,25 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, event.root.toContentStringWithIndent()))
|
add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, 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)
|
||||||
this.add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, decryptedContent))
|
add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, decryptedContent))
|
||||||
}
|
}
|
||||||
this.add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, event.root.eventId))
|
add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, event.root.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
|
||||||
this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, event.root.eventId))
|
add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, event.root.eventId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return actions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun canCancel(event: TimelineEvent): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
|
private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
|
||||||
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||||
|
@ -232,6 +238,11 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
|
||||||
return event.root.senderId == myUserId
|
return event.root.senderId == myUserId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun canRetry(event: TimelineEvent): Boolean {
|
||||||
|
return event.root.sendState.hasFailed() && event.root.isTextMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun canViewReactions(event: TimelineEvent): Boolean {
|
private fun canViewReactions(event: TimelineEvent): Boolean {
|
||||||
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||||
|
|
|
@ -43,7 +43,7 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
|
||||||
val informationData = MessageInformationData(
|
val informationData = MessageInformationData(
|
||||||
eventId = event.root.eventId ?: "?",
|
eventId = event.root.eventId ?: "?",
|
||||||
senderId = event.root.senderId ?: "",
|
senderId = event.root.senderId ?: "",
|
||||||
sendState = event.sendState,
|
sendState = event.root.sendState,
|
||||||
avatarUrl = event.senderAvatar(),
|
avatarUrl = event.senderAvatar(),
|
||||||
memberName = event.senderName(),
|
memberName = event.senderName(),
|
||||||
showInformation = false
|
showInformation = false
|
||||||
|
|
|
@ -97,7 +97,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
val informationData = MessageInformationData(
|
val informationData = MessageInformationData(
|
||||||
eventId = event.root.eventId ?: "?",
|
eventId = event.root.eventId ?: "?",
|
||||||
senderId = event.root.senderId ?: "",
|
senderId = event.root.senderId ?: "",
|
||||||
sendState = event.sendState,
|
sendState = event.root.sendState,
|
||||||
time = "",
|
time = "",
|
||||||
avatarUrl = event.senderAvatar(),
|
avatarUrl = event.senderAvatar(),
|
||||||
memberName = "",
|
memberName = "",
|
||||||
|
@ -121,7 +121,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
event.annotations?.editSummary,
|
event.annotations?.editSummary,
|
||||||
highlight,
|
highlight,
|
||||||
callback)
|
callback)
|
||||||
is MessageTextContent -> buildTextMessageItem(event.sendState,
|
is MessageTextContent -> buildTextMessageItem(event.root.sendState,
|
||||||
messageContent,
|
messageContent,
|
||||||
informationData,
|
informationData,
|
||||||
event.annotations?.editSummary,
|
event.annotations?.editSummary,
|
||||||
|
|
|
@ -37,7 +37,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
|
||||||
val informationData = MessageInformationData(
|
val informationData = MessageInformationData(
|
||||||
eventId = event.root.eventId ?: "?",
|
eventId = event.root.eventId ?: "?",
|
||||||
senderId = event.root.senderId ?: "",
|
senderId = event.root.senderId ?: "",
|
||||||
sendState = event.sendState,
|
sendState = event.root.sendState,
|
||||||
avatarUrl = event.senderAvatar(),
|
avatarUrl = event.senderAvatar(),
|
||||||
memberName = event.senderName(),
|
memberName = event.senderName(),
|
||||||
showInformation = false
|
showInformation = false
|
||||||
|
|
|
@ -76,7 +76,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||||
val informationData = MessageInformationData(
|
val informationData = MessageInformationData(
|
||||||
eventId = event.root.eventId ?: "?",
|
eventId = event.root.eventId ?: "?",
|
||||||
senderId = event.root.senderId ?: "",
|
senderId = event.root.senderId ?: "",
|
||||||
sendState = event.sendState,
|
sendState = event.root.sendState,
|
||||||
time = "",
|
time = "",
|
||||||
avatarUrl = event.senderAvatar(),
|
avatarUrl = event.senderAvatar(),
|
||||||
memberName = "",
|
memberName = "",
|
||||||
|
|
|
@ -162,10 +162,15 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun renderSendState(root: View, textView: TextView?) {
|
protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) {
|
||||||
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) {
|
||||||
|
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) {
|
||||||
|
|
|
@ -43,14 +43,20 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
imageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView)
|
imageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView)
|
||||||
contentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
|
if (!informationData.sendState.hasFailed()) {
|
||||||
|
contentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
|
||||||
|
}
|
||||||
holder.imageView.setOnClickListener(clickListener)
|
holder.imageView.setOnClickListener(clickListener)
|
||||||
holder.imageView.setOnLongClickListener(longClickListener)
|
holder.imageView.setOnLongClickListener(longClickListener)
|
||||||
ViewCompat.setTransitionName(holder.imageView,"imagePreview_${id()}")
|
ViewCompat.setTransitionName(holder.imageView,"imagePreview_${id()}")
|
||||||
holder.mediaContentView.setOnClickListener(cellClickListener)
|
holder.mediaContentView.setOnClickListener(cellClickListener)
|
||||||
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)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +73,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||||
val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
|
val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
|
||||||
|
|
||||||
val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia)
|
val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia)
|
||||||
|
val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -64,7 +64,7 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate
|
||||||
return MessageInformationData(
|
return MessageInformationData(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
senderId = event.root.senderId ?: "",
|
senderId = event.root.senderId ?: "",
|
||||||
sendState = event.sendState,
|
sendState = event.root.sendState,
|
||||||
time = time,
|
time = time,
|
||||||
avatarUrl = avatarUrl,
|
avatarUrl = avatarUrl,
|
||||||
memberName = formattedMemberName,
|
memberName = formattedMemberName,
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="22dp"
|
||||||
|
android:height="19dp"
|
||||||
|
android:viewportWidth="22"
|
||||||
|
android:viewportHeight="19">
|
||||||
|
<path
|
||||||
|
android:pathData="M21,2.741v5.333h-5.455M1,16.963V11.63h5.455"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#9E9E9E"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M3.282,7.185c0.937,-2.589 3.167,-4.527 5.907,-5.133 2.74,-0.607 5.607,0.204 7.593,2.147L21,8.074M1,11.63l4.218,3.875c1.986,1.943 4.853,2.754 7.593,2.148 2.74,-0.606 4.97,-2.545 5.907,-5.134"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#9E9E9E"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="20dp"
|
||||||
|
android:height="23dp"
|
||||||
|
android:viewportWidth="20"
|
||||||
|
android:viewportHeight="23">
|
||||||
|
<path
|
||||||
|
android:pathData="M1,5.852h18M17,5.852v14a2,2 0,0 1,-2 2H5a2,2 0,0 1,-2 -2v-14m3,0v-2a2,2 0,0 1,2 -2h4a2,2 0,0 1,2 2v2M8,10.852v6M12,10.852v6"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#9E9E9E"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="14dp"
|
||||||
|
android:height="14dp"
|
||||||
|
android:viewportWidth="14"
|
||||||
|
android:viewportHeight="14">
|
||||||
|
<path
|
||||||
|
android:pathData="M7,12.852A6,6 0,1 0,7 0.852a6,6 0,0 0,0 12zM7,1.452a1.8,1.8 0,0 1,1.8 1.8L8.8,6.852a1.8,1.8 0,1 1,-3.6 0L5.2,3.252A1.8,1.8 0,0 1,7 1.452zM7,12.252a1.8,1.8 0,1 0,0 -3.6,1.8 1.8,0 0,0 0,3.6z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="1.44"
|
||||||
|
android:fillColor="#FF4B55"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#FF4B55"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
|
@ -87,6 +87,38 @@
|
||||||
tools:text="Friday 8pm" />
|
tools:text="Friday 8pm" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/messageStatusInfo"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:layout_marginEnd="16dp">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/messageStatusProgress"
|
||||||
|
style="?android:attr/progressBarStyleSmall"
|
||||||
|
android:layout_width="12dp"
|
||||||
|
android:layout_height="12dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/messageStatusText"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:drawableStart="@drawable/ic_warning_small"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
tools:text="@string/unable_to_send_message" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/quickReactTopDivider"
|
android:id="@+id/quickReactTopDivider"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -93,8 +93,6 @@
|
||||||
android:paddingTop="16dp"
|
android:paddingTop="16dp"
|
||||||
android:paddingBottom="16dp"
|
android:paddingBottom="16dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollView" />
|
app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollView" />
|
||||||
|
|
||||||
|
@ -113,7 +111,6 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginLeft="16dp"
|
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:minHeight="@dimen/layout_touch_size"
|
android:minHeight="@dimen/layout_touch_size"
|
||||||
|
|
|
@ -15,7 +15,19 @@
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:layout_height="300dp" />
|
tools:layout_height="300dp"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/messageFailToSendIndicator"
|
||||||
|
android:layout_width="14dp"
|
||||||
|
android:layout_height="14dp"
|
||||||
|
android:layout_marginStart="2dp"
|
||||||
|
android:src="@drawable/ic_warning_small"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/messageThumbnailView"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/messageThumbnailView"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/messageMediaPlayView"
|
android:id="@+id/messageMediaPlayView"
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu 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">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/resend_all"
|
||||||
|
android:icon="@drawable/ic_refresh_cw"
|
||||||
|
android:title="@string/room_prompt_resend"
|
||||||
|
android:visible="false"
|
||||||
|
app:showAsAction="never"
|
||||||
|
tools:visible="true" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/clear_all"
|
||||||
|
android:icon="@drawable/ic_trash"
|
||||||
|
android:title="@string/room_prompt_cancel"
|
||||||
|
android:visible="false"
|
||||||
|
app:showAsAction="never"
|
||||||
|
tools:visible="true" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/clear_message_queue"
|
||||||
|
android:title="@string/clear_timeline_send_queue"
|
||||||
|
android:visible="false"
|
||||||
|
app:showAsAction="never"
|
||||||
|
tools:visible="true" />
|
||||||
|
|
||||||
|
</menu>
|
|
@ -1,89 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_vector_resend_message"
|
|
||||||
android:icon="@drawable/ic_material_send_black"
|
|
||||||
android:title="@string/resend"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_vector_cancel_upload"
|
|
||||||
android:icon="@drawable/vector_cancel_upload_download"
|
|
||||||
android:title="@string/room_event_action_cancel_upload"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_vector_cancel_download"
|
|
||||||
android:icon="@drawable/vector_cancel_upload_download"
|
|
||||||
android:title="@string/room_event_action_cancel_download"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_vector_redact_message"
|
|
||||||
android:icon="@drawable/ic_material_delete"
|
|
||||||
android:title="@string/redact"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_vector_copy"
|
|
||||||
android:icon="@drawable/ic_material_copy"
|
|
||||||
android:title="@string/copy"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_vector_quote"
|
|
||||||
android:icon="@drawable/ic_material_quote"
|
|
||||||
android:title="@string/quote"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_vector_share"
|
|
||||||
android:icon="@drawable/ic_material_share"
|
|
||||||
android:title="@string/share"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_vector_forward"
|
|
||||||
android:icon="@drawable/ic_material_forward"
|
|
||||||
android:title="@string/forward"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_vector_save"
|
|
||||||
android:icon="@drawable/ic_material_save"
|
|
||||||
android:title="@string/save"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_view_source"
|
|
||||||
android:icon="@drawable/ic_material_message_black"
|
|
||||||
android:title="@string/view_source"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_view_decrypted_source"
|
|
||||||
android:icon="@drawable/ic_material_message_black"
|
|
||||||
android:title="@string/view_decrypted_source"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_vector_permalink"
|
|
||||||
android:icon="@drawable/ic_material_link_black"
|
|
||||||
android:title="@string/permalink"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_vector_report"
|
|
||||||
android:icon="@drawable/ic_report_black"
|
|
||||||
android:title="@string/report_content"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/ic_action_device_verification"
|
|
||||||
android:icon="@drawable/ic_perm_device_information_black"
|
|
||||||
android:title="@string/device_information"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
</menu>
|
|
|
@ -507,7 +507,7 @@
|
||||||
<string name="room_unsent_messages_notification">Messages not sent. %1$s or %2$s now?</string>
|
<string name="room_unsent_messages_notification">Messages not sent. %1$s or %2$s now?</string>
|
||||||
<string name="room_unknown_devices_messages_notification">Messages not sent due to unknown devices being present. %1$s or %2$s now?</string>
|
<string name="room_unknown_devices_messages_notification">Messages not sent due to unknown devices being present. %1$s or %2$s now?</string>
|
||||||
<string name="room_prompt_resend">Resend all</string>
|
<string name="room_prompt_resend">Resend all</string>
|
||||||
<string name="room_prompt_cancel">cancel all</string>
|
<string name="room_prompt_cancel">Cancel all</string>
|
||||||
<string name="room_resend_unsent_messages">Resend unsent messages</string>
|
<string name="room_resend_unsent_messages">Resend unsent messages</string>
|
||||||
<string name="room_delete_unsent_messages">Delete unsent messages</string>
|
<string name="room_delete_unsent_messages">Delete unsent messages</string>
|
||||||
<string name="room_message_file_not_found">File not found</string>
|
<string name="room_message_file_not_found">File not found</string>
|
||||||
|
|
Loading…
Reference in New Issue