Scroll when event build come from sync/send + remove use of monarchy writeAsync

This commit is contained in:
ganfra 2020-01-20 21:13:53 +01:00
parent 76065ac4fc
commit fee2ec6b66
22 changed files with 538 additions and 275 deletions

View File

@ -17,18 +17,20 @@
package im.vector.matrix.android.api.session.room.send package im.vector.matrix.android.api.session.room.send
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
interface DraftService { interface DraftService {
/** /**
* Save or update a draft to the room * Save or update a draft to the room
*/ */
fun saveDraft(draft: UserDraft) fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable
/** /**
* Delete the last draft, basically just after sending the message * Delete the last draft, basically just after sending the message
*/ */
fun deleteDraft() fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable
/** /**
* Return the current drafts if any, as a live data * Return the current drafts if any, as a live data

View File

@ -112,6 +112,11 @@ interface Timeline {
* Called whenever an error we can't recover from occurred * Called whenever an error we can't recover from occurred
*/ */
fun onTimelineFailure(throwable: Throwable) fun onTimelineFailure(throwable: Throwable)
/**
* Call when new events come through the sync
*/
fun onNewTimelineEvents(eventIds: List<String>)
} }
/** /**

View File

@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.fastContains import im.vector.matrix.android.internal.database.query.fastContains
import im.vector.matrix.android.internal.extensions.assertIsManaged import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import io.realm.Realm
internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) { internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
chunks.remove(chunkEntity) chunks.remove(chunkEntity)
@ -53,8 +54,7 @@ internal fun RoomEntity.addStateEvent(stateEvent: Event,
untimelinedStateEvents.add(entity) untimelinedStateEvents.add(entity)
} }
} }
internal fun RoomEntity.addSendingEvent(event: Event) { internal fun RoomEntity.addSendingEvent(realm: Realm, event: Event) {
assertIsManaged()
val senderId = event.senderId ?: return val senderId = event.senderId ?: return
val eventEntity = event.toEntity(roomId).apply { val eventEntity = event.toEntity(roomId).apply {
this.sendState = SendState.UNSENT this.sendState = SendState.UNSENT
@ -72,3 +72,4 @@ internal fun RoomEntity.addSendingEvent(event: Event) {
} }
sendingTimelineEvents.add(0, timelineEventEntity) sendingTimelineEvents.add(0, timelineEventEntity)
} }

View File

@ -43,9 +43,12 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
senderName = timelineEventEntity.senderName, senderName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
senderAvatar = timelineEventEntity.senderAvatar, senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = readReceipts?.sortedByDescending { readReceipts = readReceipts
it.originServerTs ?.distinctBy {
} ?: emptyList() it.user
}?.sortedByDescending {
it.originServerTs
} ?: emptyList()
) )
} }
} }

View File

@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.session.group.model.GroupRooms
import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse
import im.vector.matrix.android.internal.session.group.model.GroupUsers import im.vector.matrix.android.internal.session.group.model.GroupUsers
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import javax.inject.Inject import javax.inject.Inject
@ -53,12 +54,12 @@ internal class DefaultGetGroupDataTask @Inject constructor(
insertInDb(groupSummary, groupRooms, groupUsers, groupId) insertInDb(groupSummary, groupRooms, groupUsers, groupId)
} }
private fun insertInDb(groupSummary: GroupSummaryResponse, private suspend fun insertInDb(groupSummary: GroupSummaryResponse,
groupRooms: GroupRooms, groupRooms: GroupRooms,
groupUsers: GroupUsers, groupUsers: GroupUsers,
groupId: String) { groupId: String) {
monarchy monarchy
.writeAsync { realm -> .awaitTransaction { realm ->
val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst() val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst()
?: realm.createObject(GroupSummaryEntity::class.java, groupId) ?: realm.createObject(GroupSummaryEntity::class.java, groupId)

View File

@ -50,25 +50,25 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
private val typingServiceFactory: DefaultTypingService.Factory, private val typingServiceFactory: DefaultTypingService.Factory,
private val relationServiceFactory: DefaultRelationService.Factory, private val relationServiceFactory: DefaultRelationService.Factory,
private val membershipServiceFactory: DefaultMembershipService.Factory, private val membershipServiceFactory: DefaultMembershipService.Factory,
private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory) : private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory):
RoomFactory { RoomFactory {
override fun create(roomId: String): Room { override fun create(roomId: String): Room {
return DefaultRoom( return DefaultRoom(
roomId, roomId = roomId,
monarchy, monarchy = monarchy,
roomSummaryMapper, roomSummaryMapper = roomSummaryMapper,
timelineServiceFactory.create(roomId), timelineService = timelineServiceFactory.create(roomId),
sendServiceFactory.create(roomId), sendService = sendServiceFactory.create(roomId),
draftServiceFactory.create(roomId), draftService = draftServiceFactory.create(roomId),
stateServiceFactory.create(roomId), stateService = stateServiceFactory.create(roomId),
reportingServiceFactory.create(roomId), reportingService = reportingServiceFactory.create(roomId),
readServiceFactory.create(roomId), readService = readServiceFactory.create(roomId),
typingServiceFactory.create(roomId), typingService = typingServiceFactory.create(roomId),
cryptoService, cryptoService = cryptoService,
relationServiceFactory.create(roomId), relationService = relationServiceFactory.create(roomId),
membershipServiceFactory.create(roomId), roomMembersService = membershipServiceFactory.create(roomId),
roomPushRuleServiceFactory.create(roomId) roomPushRuleService = roomPushRuleServiceFactory.create(roomId)
) )
} }
} }

View File

@ -17,23 +17,21 @@
package im.vector.matrix.android.internal.session.room.draft package im.vector.matrix.android.internal.session.room.draft
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.internal.database.mapper.DraftMapper import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.model.DraftEntity import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.database.model.UserDraftsEntity import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.database.query.where
import io.realm.kotlin.createObject
import timber.log.Timber
internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String, internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy private val draftRepository: DraftRepository,
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers
) : DraftService { ) : DraftService {
@AssistedInject.Factory @AssistedInject.Factory
@ -45,121 +43,19 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private
* The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft, * The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft,
* or even move an existing draft to the top of the list * or even move an existing draft to the top of the list
*/ */
override fun saveDraft(draft: UserDraft) { override fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable {
Timber.d("Draft: saveDraft ${privacySafe(draft)}") return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
draftRepository.saveDraft(roomId, draft)
monarchy.writeAsync { realm ->
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
val userDraftsEntity = roomSummaryEntity.userDrafts
?: realm.createObject<UserDraftsEntity>().also {
roomSummaryEntity.userDrafts = it
}
userDraftsEntity.let { userDraftEntity ->
// Save only valid draft
if (draft.isValid()) {
// Add a new draft or update the current one?
val newDraft = DraftMapper.map(draft)
// Is it an update of the top draft?
val topDraft = userDraftEntity.userDrafts.lastOrNull()
if (topDraft == null) {
Timber.d("Draft: create a new draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
} else if (topDraft.draftMode == DraftEntity.MODE_EDIT) {
// top draft is an edit
if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
if (topDraft.linkedEventId == newDraft.linkedEventId) {
// Update the top draft
Timber.d("Draft: update the top edit draft ${privacySafe(draft)}")
topDraft.content = newDraft.content
} else {
// Check a previously EDIT draft with the same id
val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find {
it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId
}
if (existingEditDraftOfSameEvent != null) {
// Ignore the new text, restore what was typed before, by putting the draft to the top
Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent)
userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent)
} else {
Timber.d("Draft: add a new edit draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
}
}
} else {
// Add a new regular draft to the top
Timber.d("Draft: add a new draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
}
} else {
// Top draft is not an edit
if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
Timber.d("Draft: create a new edit draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
} else {
// Update the top draft
Timber.d("Draft: update the top draft ${privacySafe(draft)}")
topDraft.draftMode = newDraft.draftMode
topDraft.content = newDraft.content
topDraft.linkedEventId = newDraft.linkedEventId
}
}
} else {
// There is no draft to save, so the composer was clear
Timber.d("Draft: delete a draft")
val topDraft = userDraftEntity.userDrafts.lastOrNull()
if (topDraft == null) {
Timber.d("Draft: nothing to do")
} else {
// Remove the top draft
Timber.d("Draft: remove the top draft")
userDraftEntity.userDrafts.remove(topDraft)
}
}
}
} }
} }
private fun privacySafe(o: Any): Any { override fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable {
if (BuildConfig.LOG_PRIVATE_DATA) { return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
return o draftRepository.deleteDraft(roomId)
}
return ""
}
override fun deleteDraft() {
Timber.d("Draft: deleteDraft()")
monarchy.writeAsync { realm ->
UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity ->
if (userDraftsEntity.userDrafts.isNotEmpty()) {
userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1)
}
}
} }
} }
override fun getDraftsLive(): LiveData<List<UserDraft>> { override fun getDraftsLive(): LiveData<List<UserDraft>> {
val liveData = monarchy.findAllMappedWithChanges( return draftRepository.getDraftsLive(roomId)
{ UserDraftsEntity.where(it, roomId) },
{
it.userDrafts.map { draft ->
DraftMapper.map(draft)
}
}
)
return Transformations.map(liveData) {
it.firstOrNull() ?: emptyList()
}
} }
} }

View File

@ -0,0 +1,156 @@
/*
* Copyright 2020 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.draft
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.internal.database.mapper.DraftMapper
import im.vector.matrix.android.internal.database.model.DraftEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.UserDraftsEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm
import io.realm.kotlin.createObject
import timber.log.Timber
import javax.inject.Inject
class DraftRepository @Inject constructor(private val monarchy: Monarchy) {
suspend fun saveDraft(roomId: String, userDraft: UserDraft) {
monarchy.awaitTransaction {
saveDraft(it, userDraft, roomId)
}
}
suspend fun deleteDraft(roomId: String) {
monarchy.awaitTransaction {
deleteDraft(it, roomId)
}
}
private fun deleteDraft(realm: Realm, roomId: String) {
UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity ->
if (userDraftsEntity.userDrafts.isNotEmpty()) {
userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1)
}
}
}
private fun saveDraft(realm: Realm, draft: UserDraft, roomId: String) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
val userDraftsEntity = roomSummaryEntity.userDrafts
?: realm.createObject<UserDraftsEntity>().also {
roomSummaryEntity.userDrafts = it
}
userDraftsEntity.let { userDraftEntity ->
// Save only valid draft
if (draft.isValid()) {
// Add a new draft or update the current one?
val newDraft = DraftMapper.map(draft)
// Is it an update of the top draft?
val topDraft = userDraftEntity.userDrafts.lastOrNull()
if (topDraft == null) {
Timber.d("Draft: create a new draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
} else if (topDraft.draftMode == DraftEntity.MODE_EDIT) {
// top draft is an edit
if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
if (topDraft.linkedEventId == newDraft.linkedEventId) {
// Update the top draft
Timber.d("Draft: update the top edit draft ${privacySafe(draft)}")
topDraft.content = newDraft.content
} else {
// Check a previously EDIT draft with the same id
val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find {
it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId
}
if (existingEditDraftOfSameEvent != null) {
// Ignore the new text, restore what was typed before, by putting the draft to the top
Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent)
userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent)
} else {
Timber.d("Draft: add a new edit draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
}
}
} else {
// Add a new regular draft to the top
Timber.d("Draft: add a new draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
}
} else {
// Top draft is not an edit
if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
Timber.d("Draft: create a new edit draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
} else {
// Update the top draft
Timber.d("Draft: update the top draft ${privacySafe(draft)}")
topDraft.draftMode = newDraft.draftMode
topDraft.content = newDraft.content
topDraft.linkedEventId = newDraft.linkedEventId
}
}
} else {
// There is no draft to save, so the composer was clear
Timber.d("Draft: delete a draft")
val topDraft = userDraftEntity.userDrafts.lastOrNull()
if (topDraft == null) {
Timber.d("Draft: nothing to do")
} else {
// Remove the top draft
Timber.d("Draft: remove the top draft")
userDraftEntity.userDrafts.remove(topDraft)
}
}
}
}
fun getDraftsLive(roomId: String): LiveData<List<UserDraft>> {
val liveData = monarchy.findAllMappedWithChanges(
{ UserDraftsEntity.where(it, roomId) },
{
it.userDrafts.map { draft ->
DraftMapper.map(draft)
}
}
)
return Transformations.map(liveData) {
it.firstOrNull() ?: emptyList()
}
}
private fun privacySafe(o: Any): Any {
if (BuildConfig.LOG_PRIVATE_DATA) {
return o
}
return ""
}
}

View File

@ -242,6 +242,6 @@ internal class DefaultRelationService @AssistedInject constructor(
* the same transaction id is received (in unsigned data) * the same transaction id is received (in unsigned data)
*/ */
private fun saveLocalEcho(event: Event) { private fun saveLocalEcho(event: Event) {
eventFactory.saveLocalEcho(monarchy, event) eventFactory.createLocalEcho(event)
} }
} }

View File

@ -17,35 +17,35 @@
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.* import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.Operation
import androidx.work.WorkManager
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
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.* import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.events.model.isImageMessage
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.events.model.isTextMessage
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.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.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.findAllInRoomWithSendStates
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.SessionId
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.task.TaskExecutor
import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.CancelableWork
import im.vector.matrix.android.internal.worker.AlwaysSuccessfulWorker import im.vector.matrix.android.internal.worker.AlwaysSuccessfulWorker
import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil
import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.startChain import im.vector.matrix.android.internal.worker.startChain
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -59,7 +59,9 @@ internal class DefaultSendService @AssistedInject constructor(
@SessionId private val sessionId: String, @SessionId private val sessionId: String,
private val localEchoEventFactory: LocalEchoEventFactory, private val localEchoEventFactory: LocalEchoEventFactory,
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val monarchy: Monarchy private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository
) : SendService { ) : SendService {
@AssistedInject.Factory @AssistedInject.Factory
@ -71,15 +73,14 @@ internal class DefaultSendService @AssistedInject constructor(
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable { override fun sendTextMessage(text: CharSequence, 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) createLocalEcho(it)
} }
return sendEvent(event) return sendEvent(event)
} }
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also { val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also {
saveLocalEcho(it) createLocalEcho(it)
} }
return sendEvent(event) return sendEvent(event)
@ -157,13 +158,8 @@ internal class DefaultSendService @AssistedInject constructor(
} }
override fun deleteFailedEcho(localEcho: TimelineEvent) { override fun deleteFailedEcho(localEcho: TimelineEvent) {
monarchy.writeAsync { realm -> taskExecutor.executorScope.launch {
TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.let { localEchoRepository.deleteFailedEcho(roomId, localEcho)
it.deleteFromRealm()
}
EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let {
it.deleteFromRealm()
}
} }
} }
@ -181,67 +177,26 @@ internal class DefaultSendService @AssistedInject constructor(
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it) .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it)
.enqueue() .enqueue()
} }
taskExecutor.executorScope.launch {
monarchy.writeAsync { realm -> localEchoRepository.clearSendingQueue(roomId)
RoomEntity.where(realm, roomId).findFirst()?.let { room ->
room.sendingTimelineEvents.forEach {
it.root?.sendState = SendState.UNDELIVERED
}
}
} }
} }
override fun resendAllFailedMessages() { override fun resendAllFailedMessages() {
monarchy.writeAsync { realm -> taskExecutor.executorScope.launch {
TimelineEventEntity val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId)
.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) eventsToResend.forEach {
.sortedBy { it.root?.originServerTs ?: 0 } sendEvent(it)
.forEach { timelineEventEntity -> }
timelineEventEntity.root?.let { localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
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) createLocalEcho(it)
} }
return internalSendMedia(event, attachment) return internalSendMedia(event, attachment)
} }
@ -276,8 +231,8 @@ internal class DefaultSendService @AssistedInject constructor(
return CancelableWork(context, sendWork.id) return CancelableWork(context, sendWork.id)
} }
private fun saveLocalEcho(event: Event) { private fun createLocalEcho(event: Event) {
localEchoEventFactory.saveLocalEcho(monarchy, event) localEchoEventFactory.createLocalEcho(event)
} }
private fun buildWorkName(identifier: String): String { private fun buildWorkName(identifier: String): String {
@ -306,7 +261,7 @@ internal class DefaultSendService @AssistedInject constructor(
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also {
saveLocalEcho(it) createLocalEcho(it)
} }
val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason) val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason)
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)

View File

@ -18,26 +18,42 @@ package im.vector.matrix.android.internal.session.room.send
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R import im.vector.matrix.android.R
import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.permalinks.PermalinkFactory
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.* import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.UnsignedData
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.AudioInfo
import im.vector.matrix.android.api.session.room.model.message.FileInfo
import im.vector.matrix.android.api.session.room.model.message.ImageInfo
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo
import im.vector.matrix.android.api.session.room.model.message.VideoInfo
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.database.helper.addSendingEvent import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.StringProvider import im.vector.matrix.android.internal.util.StringProvider
import kotlinx.coroutines.launch
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
import javax.inject.Inject import javax.inject.Inject
@ -54,8 +70,9 @@ import javax.inject.Inject
internal class LocalEchoEventFactory @Inject constructor( internal class LocalEchoEventFactory @Inject constructor(
@UserId private val userId: String, @UserId private val userId: String,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val roomSummaryUpdater: RoomSummaryUpdater, private val textPillsUtils: TextPillsUtils,
private val textPillsUtils: TextPillsUtils private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository
) { ) {
// TODO Inject // TODO Inject
private val parser = Parser.builder().build() private val parser = Parser.builder().build()
@ -402,13 +419,10 @@ internal class LocalEchoEventFactory @Inject constructor(
) )
} }
fun saveLocalEcho(monarchy: Monarchy, event: Event) { fun createLocalEcho(event: Event){
checkNotNull(event.roomId) { "Your event should have a roomId" } checkNotNull(event.roomId) { "Your event should have a roomId" }
monarchy.writeAsync { realm -> taskExecutor.executorScope.launch {
val roomEntity = RoomEntity.where(realm, roomId = event.roomId).findFirst() localEchoRepository.createLocalEcho(event)
?: return@writeAsync
roomEntity.addSendingEvent(event)
roomSummaryUpdater.update(realm, event.roomId)
} }
} }

View File

@ -0,0 +1,147 @@
/*
* Copyright 2020 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 com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.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.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.helper.nextId
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.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.findAllInRoomWithSendStates
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
internal class LocalEchoRepository @Inject constructor(private val monarchy: Monarchy,
private val roomSummaryUpdater: RoomSummaryUpdater,
private val eventBus: EventBus) {
suspend fun createLocalEcho(event: Event) {
val roomId = event.roomId ?: return
val senderId = event.senderId ?: return
val eventId = event.eventId ?: return
eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = listOf(eventId)))
monarchy.awaitTransaction { realm ->
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@awaitTransaction
val eventEntity = event.toEntity(roomId).apply {
this.sendState = SendState.UNSENT
}
val roomMemberHelper = RoomMemberHelper(realm, roomId)
val myUser = roomMemberHelper.getLastRoomMember(senderId)
val localId = TimelineEventEntity.nextId(realm)
val timelineEventEntity = TimelineEventEntity(localId).also {
it.root = eventEntity
it.eventId = event.eventId
it.roomId = roomId
it.senderName = myUser?.displayName
it.senderAvatar = myUser?.avatarUrl
it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(myUser?.displayName)
}
roomEntity.sendingTimelineEvents.add(0, timelineEventEntity)
roomSummaryUpdater.update(realm, roomId)
}
}
suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) {
monarchy.awaitTransaction { realm ->
TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm()
EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let {
it.deleteFromRealm()
}
}
}
suspend fun clearSendingQueue(roomId: String) {
monarchy.awaitTransaction { realm ->
RoomEntity.where(realm, roomId).findFirst()?.let { room ->
room.sendingTimelineEvents.forEach {
it.root?.sendState = SendState.UNDELIVERED
}
}
}
}
suspend fun updateSendState(roomId: String, eventIds: List<String>, sendState: SendState) {
monarchy.awaitTransaction { realm ->
val timelineEvents = TimelineEventEntity.where(realm, roomId, eventIds).findAll()
timelineEvents.forEach {
it.root?.sendState = sendState
}
}
}
fun getAllFailedEventsToResend(roomId: String): List<Event> {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
TimelineEventEntity
.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES)
.sortedByDescending { it.root?.displayIndex ?: 0 }
.mapNotNull { it.root?.asDomain() }
.filter { event ->
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 -> {
true
}
MessageType.MSGTYPE_FILE,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO -> {
// need to resend the attachement
false
}
else -> {
Timber.e("Cannot resend message ${event.type} / ${content.type}")
false
}
}
} else {
Timber.e("Unsupported message to resend ${event.type}")
false
}
}
else -> {
Timber.e("Unsupported message to resend ${event.type}")
false
}
}
}
}
}
}

View File

@ -50,6 +50,9 @@ import io.realm.RealmConfiguration
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.Sort import io.realm.Sort
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber import timber.log.Timber
import java.util.Collections import java.util.Collections
import java.util.UUID import java.util.UUID
@ -72,9 +75,12 @@ internal class DefaultTimeline(
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val settings: TimelineSettings, private val settings: TimelineSettings,
private val hiddenReadReceipts: TimelineHiddenReadReceipts private val hiddenReadReceipts: TimelineHiddenReadReceipts,
private val eventBus: EventBus
) : Timeline, TimelineHiddenReadReceipts.Delegate { ) : Timeline, TimelineHiddenReadReceipts.Delegate {
data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>)
companion object { companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
} }
@ -128,7 +134,7 @@ internal class DefaultTimeline(
if (hasChange) postSnapshot() if (hasChange) postSnapshot()
} }
// Public methods ****************************************************************************** // Public methods ******************************************************************************
override fun paginate(direction: Timeline.Direction, count: Int) { override fun paginate(direction: Timeline.Direction, count: Int) {
BACKGROUND_HANDLER.post { BACKGROUND_HANDLER.post {
@ -159,6 +165,7 @@ internal class DefaultTimeline(
override fun start() { override fun start() {
if (isStarted.compareAndSet(false, true)) { if (isStarted.compareAndSet(false, true)) {
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
eventBus.register(this)
BACKGROUND_HANDLER.post { BACKGROUND_HANDLER.post {
eventDecryptor.start() eventDecryptor.start()
val realm = Realm.getInstance(realmConfiguration) val realm = Realm.getInstance(realmConfiguration)
@ -190,12 +197,13 @@ internal class DefaultTimeline(
} }
private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean { private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean {
return settings.buildReadReceipts && (settings.filterEdits || settings.filterTypes) return buildReadReceipts && (filterEdits || filterTypes)
} }
override fun dispose() { override fun dispose() {
if (isStarted.compareAndSet(true, false)) { if (isStarted.compareAndSet(true, false)) {
isReady.set(false) isReady.set(false)
eventBus.unregister(this)
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
cancelableBag.cancel() cancelableBag.cancel()
BACKGROUND_HANDLER.removeCallbacksAndMessages(null) BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
@ -316,6 +324,15 @@ internal class DefaultTimeline(
postSnapshot() postSnapshot()
} }
@Subscribe(threadMode = ThreadMode.MAIN)
fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) {
if (onNewTimelineEvents.roomId == roomId) {
listeners.forEach {
it.onNewTimelineEvents(onNewTimelineEvents.eventIds)
}
}
}
// Private methods ***************************************************************************** // Private methods *****************************************************************************
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
@ -401,14 +418,14 @@ internal class DefaultTimeline(
private fun getState(direction: Timeline.Direction): State { private fun getState(direction: Timeline.Direction): State {
return when (direction) { return when (direction) {
Timeline.Direction.FORWARDS -> forwardsState.get() Timeline.Direction.FORWARDS -> forwardsState.get()
Timeline.Direction.BACKWARDS -> backwardsState.get() Timeline.Direction.BACKWARDS -> backwardsState.get()
} }
} }
private fun updateState(direction: Timeline.Direction, update: (State) -> State) { private fun updateState(direction: Timeline.Direction, update: (State) -> State) {
val stateReference = when (direction) { val stateReference = when (direction) {
Timeline.Direction.FORWARDS -> forwardsState Timeline.Direction.FORWARDS -> forwardsState
Timeline.Direction.BACKWARDS -> backwardsState Timeline.Direction.BACKWARDS -> backwardsState
} }
val currentValue = stateReference.get() val currentValue = stateReference.get()
@ -506,10 +523,10 @@ internal class DefaultTimeline(
this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> { this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: TokenChunkEventPersistor.Result) { override fun onSuccess(data: TokenChunkEventPersistor.Result) {
when (data) { when (data) {
TokenChunkEventPersistor.Result.SUCCESS -> { TokenChunkEventPersistor.Result.SUCCESS -> {
Timber.v("Success fetching $limit items $direction from pagination request") Timber.v("Success fetching $limit items $direction from pagination request")
} }
TokenChunkEventPersistor.Result.REACHED_END -> { TokenChunkEventPersistor.Result.REACHED_END -> {
postSnapshot() postSnapshot()
} }
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->

View File

@ -34,9 +34,11 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.fetchCopyMap import im.vector.matrix.android.internal.util.fetchCopyMap
import org.greenrobot.eventbus.EventBus
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val eventBus: EventBus,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask, private val contextOfEventTask: GetContextOfEventTask,
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
@ -52,17 +54,19 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
} }
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
return DefaultTimeline(roomId, return DefaultTimeline(
eventId, roomId = roomId,
monarchy.realmConfiguration, initialEventId = eventId,
taskExecutor, realmConfiguration = monarchy.realmConfiguration,
contextOfEventTask, taskExecutor = taskExecutor,
clearUnlinkedEventsTask, contextOfEventTask = contextOfEventTask,
paginationTask, clearUnlinkedEventsTask = clearUnlinkedEventsTask,
cryptoService, paginationTask = paginationTask,
timelineEventMapper, cryptoService = cryptoService,
settings, timelineEventMapper = timelineEventMapper,
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings) settings = settings,
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
eventBus = eventBus
) )
} }

View File

@ -36,11 +36,13 @@ import im.vector.matrix.android.internal.session.mapWithProgress
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler
import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.room.typing.TypingEventContent import im.vector.matrix.android.internal.session.room.typing.TypingEventContent
import im.vector.matrix.android.internal.session.sync.model.* import im.vector.matrix.android.internal.session.sync.model.*
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -50,7 +52,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val roomFullyReadHandler: RoomFullyReadHandler, private val roomFullyReadHandler: RoomFullyReadHandler,
private val cryptoService: DefaultCryptoService, private val cryptoService: DefaultCryptoService,
private val roomMemberEventHandler: RoomMemberEventHandler, private val roomMemberEventHandler: RoomMemberEventHandler,
private val timelineEventSenderVisitor: TimelineEventSenderVisitor) { private val timelineEventSenderVisitor: TimelineEventSenderVisitor,
private val eventBus: EventBus) {
sealed class HandlingStrategy { sealed class HandlingStrategy {
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy() data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
@ -130,6 +133,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
if (roomSync.timeline?.events?.isNotEmpty() == true) { if (roomSync.timeline?.events?.isNotEmpty() == true) {
val chunkEntity = handleTimelineEvents( val chunkEntity = handleTimelineEvents(
realm, realm,
roomId,
roomEntity, roomEntity,
roomSync.timeline.events, roomSync.timeline.events,
roomSync.timeline.prevToken, roomSync.timeline.prevToken,
@ -161,7 +165,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
roomEntity.membership = Membership.INVITE roomEntity.membership = Membership.INVITE
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events) val chunkEntity = handleTimelineEvents(realm, roomId, roomEntity, roomSync.inviteState.events)
roomEntity.addOrUpdate(chunkEntity) roomEntity.addOrUpdate(chunkEntity)
} }
val hasRoomMember = roomSync.inviteState?.events?.firstOrNull { val hasRoomMember = roomSync.inviteState?.events?.firstOrNull {
@ -183,6 +187,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} }
private fun handleTimelineEvents(realm: Realm, private fun handleTimelineEvents(realm: Realm,
roomId: String,
roomEntity: RoomEntity, roomEntity: RoomEntity,
eventList: List<Event>, eventList: List<Event>,
prevToken: String? = null, prevToken: String? = null,
@ -202,7 +207,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
chunkEntity.isUnlinked = false chunkEntity.isUnlinked = false
val timelineEvents = ArrayList<TimelineEventEntity>(eventList.size) val timelineEvents = ArrayList<TimelineEventEntity>(eventList.size)
val eventIds = ArrayList<String>(eventList.size)
for (event in eventList) { for (event in eventList) {
if(event.eventId != null) {
eventIds.add(event.eventId)
}
chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)?.also { chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)?.also {
timelineEvents.add(it) timelineEvents.add(it)
} }
@ -221,6 +230,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
roomMemberEventHandler.handle(realm, roomEntity.roomId, event) roomMemberEventHandler.handle(realm, roomEntity.roomId, event)
} }
timelineEventSenderVisitor.visit(timelineEvents) timelineEventSenderVisitor.visit(timelineEvents)
// posting new events to timeline if any is registered
eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = eventIds))
return chunkEntity return chunkEntity
} }

View File

@ -35,7 +35,7 @@ import kotlin.coroutines.EmptyCoroutineContext
@MatrixScope @MatrixScope
internal class TaskExecutor @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers) { internal class TaskExecutor @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers) {
private val executorScope = CoroutineScope(SupervisorJob()) val executorScope = CoroutineScope(SupervisorJob())
fun <PARAMS, RESULT> execute(task: ConfigurableTask<PARAMS, RESULT>): Cancelable { fun <PARAMS, RESULT> execute(task: ConfigurableTask<PARAMS, RESULT>): Cancelable {
return executorScope return executorScope

View File

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

View File

@ -312,6 +312,7 @@ class RoomDetailFragment @Inject constructor(
.subscribe { .subscribe {
when (it) { when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
} }
} }
.disposeOnDestroyView() .disposeOnDestroyView()

View File

@ -21,4 +21,5 @@ package im.vector.riotx.features.home.room.detail
*/ */
sealed class RoomDetailViewEvents { sealed class RoomDetailViewEvents {
data class Failure(val throwable: Throwable) : RoomDetailViewEvents() data class Failure(val throwable: Throwable) : RoomDetailViewEvents()
data class OnNewTimelineEvents(val eventIds: List<String>) : RoomDetailViewEvents()
} }

View File

@ -20,7 +20,12 @@ import android.net.Uri
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.* import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.BehaviorRelay
import com.jakewharton.rxrelay2.PublishRelay import com.jakewharton.rxrelay2.PublishRelay
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
@ -58,6 +63,7 @@ import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.core.utils.DataSource import im.vector.riotx.core.utils.DataSource
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.core.utils.NoOpMatrixCallback
import im.vector.riotx.core.utils.PublishDataSource import im.vector.riotx.core.utils.PublishDataSource
import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.core.utils.subscribeLogError
import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.CommandParser
@ -218,10 +224,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) { private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) {
withState { withState {
when (it.sendMode) { when (it.sendMode) {
is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft)) is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback())
is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft)) is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft)) is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft)) is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
} }
} }
} }
@ -465,7 +471,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
private fun popDraft() { private fun popDraft() {
room.deleteDraft() room.deleteDraft(object : MatrixCallback<Unit> {})
} }
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
@ -584,7 +590,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.EDIT(timelineEvent, action.text)) } setState { copy(sendMode = SendMode.EDIT(timelineEvent, action.text)) }
timelineEvent.root.eventId?.let { timelineEvent.root.eventId?.let {
room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: "")) room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: ""), NoOpMatrixCallback())
} }
} }
} }
@ -598,9 +604,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
// Save a new draft and keep the previously entered text, if it was not an edit // Save a new draft and keep the previously entered text, if it was not an edit
timelineEvent.root.eventId?.let { timelineEvent.root.eventId?.let {
if (state.sendMode is SendMode.EDIT) { if (state.sendMode is SendMode.EDIT) {
room.saveDraft(UserDraft.QUOTE(it, "")) room.saveDraft(UserDraft.QUOTE(it, ""), NoOpMatrixCallback())
} else { } else {
room.saveDraft(UserDraft.QUOTE(it, action.text)) room.saveDraft(UserDraft.QUOTE(it, action.text), NoOpMatrixCallback())
} }
} }
} }
@ -616,9 +622,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
// Save a new draft and keep the previously entered text, if it was not an edit // Save a new draft and keep the previously entered text, if it was not an edit
timelineEvent.root.eventId?.let { timelineEvent.root.eventId?.let {
if (state.sendMode is SendMode.EDIT) { if (state.sendMode is SendMode.EDIT) {
room.saveDraft(UserDraft.REPLY(it, "")) room.saveDraft(UserDraft.REPLY(it, ""), NoOpMatrixCallback())
} else { } else {
room.saveDraft(UserDraft.REPLY(it, action.text)) room.saveDraft(UserDraft.REPLY(it, action.text), NoOpMatrixCallback())
} }
} }
} }
@ -630,10 +636,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
withState { withState {
if (draft.isNotBlank()) { if (draft.isNotBlank()) {
when (it.sendMode) { when (it.sendMode) {
is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft)) is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft), NoOpMatrixCallback())
is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, draft)) is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback())
is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, draft)) is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback())
is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, draft)) is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback())
} }
} }
} }
@ -644,10 +650,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
withState { state -> withState { state ->
// For edit, just delete the current draft // For edit, just delete the current draft
if (state.sendMode is SendMode.EDIT) { if (state.sendMode is SendMode.EDIT) {
room.deleteDraft() room.deleteDraft(NoOpMatrixCallback())
} else { } else {
// Save a new draft and keep the previously entered text // Save a new draft and keep the previously entered text
room.saveDraft(UserDraft.REGULAR(action.text)) room.saveDraft(UserDraft.REGULAR(action.text), NoOpMatrixCallback())
} }
} }
} }
@ -892,6 +898,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
_viewEvents.post(RoomDetailViewEvents.Failure(throwable)) _viewEvents.post(RoomDetailViewEvents.Failure(throwable))
} }
override fun onNewTimelineEvents(eventIds: List<String>) {
Timber.v("On new timeline events: $eventIds")
_viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds))
}
override fun onCleared() { override fun onCleared() {
timeline.dispose() timeline.dispose()
timeline.removeAllListeners() timeline.removeAllListeners()

View File

@ -19,15 +19,29 @@ package im.vector.riotx.features.home.room.detail
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.riotx.core.platform.DefaultListUpdateCallback import im.vector.riotx.core.platform.DefaultListUpdateCallback
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem
import timber.log.Timber import timber.log.Timber
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback {
private val newTimelineEventIds = HashSet<String>()
fun addNewTimelineEventIds(eventIds: List<String>){
newTimelineEventIds.addAll(eventIds)
}
override fun onInserted(position: Int, count: Int) { override fun onInserted(position: Int, count: Int) {
Timber.v("On inserted $count count at position: $position") Timber.v("On inserted $count count at position: $position")
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) { if(layoutManager.findFirstVisibleItemPosition() != position ){
layoutManager.scrollToPosition(0) return
}
val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return
val firstNewItemIds = firstNewItem.getEventIds()
if(newTimelineEventIds.intersect(firstNewItemIds).isNotEmpty()){
Timber.v("Should scroll to position: $position")
newTimelineEventIds.clear()
layoutManager.scrollToPosition(position)
} }
} }
} }

View File

@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.epoxy.LoadingItem_
import im.vector.riotx.core.epoxy.emptyItem
import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.features.home.room.detail.RoomDetailViewState import im.vector.riotx.features.home.room.detail.RoomDetailViewState
import im.vector.riotx.features.home.room.detail.UnreadState import im.vector.riotx.features.home.room.detail.UnreadState
@ -241,6 +242,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
// no-op, already handled // no-op, already handled
} }
override fun onNewTimelineEvents(eventIds: List<String>) {
// no-op, already handled
}
private fun submitSnapshot(newSnapshot: List<TimelineEvent>) { private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
backgroundHandler.post { backgroundHandler.post {
inSubmitList = true inSubmitList = true
@ -346,8 +351,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return positionOfReadMarker return positionOfReadMarker
} }
fun isLoadingForward() = showingForwardLoader
private data class CacheItemData( private data class CacheItemData(
val localId: Long, val localId: Long,
val eventId: String?, val eventId: String?,