Scroll when event build come from sync/send + remove use of monarchy writeAsync
This commit is contained in:
parent
76065ac4fc
commit
fee2ec6b66
|
@ -17,18 +17,20 @@
|
|||
package im.vector.matrix.android.api.session.room.send
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
interface DraftService {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
fun deleteDraft()
|
||||
fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Return the current drafts if any, as a live data
|
||||
|
|
|
@ -112,6 +112,11 @@ interface Timeline {
|
|||
* Called whenever an error we can't recover from occurred
|
||||
*/
|
||||
fun onTimelineFailure(throwable: Throwable)
|
||||
|
||||
/**
|
||||
* Call when new events come through the sync
|
||||
*/
|
||||
fun onNewTimelineEvents(eventIds: List<String>)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.extensions.assertIsManaged
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
||||
import io.realm.Realm
|
||||
|
||||
internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
|
||||
chunks.remove(chunkEntity)
|
||||
|
@ -53,8 +54,7 @@ internal fun RoomEntity.addStateEvent(stateEvent: Event,
|
|||
untimelinedStateEvents.add(entity)
|
||||
}
|
||||
}
|
||||
internal fun RoomEntity.addSendingEvent(event: Event) {
|
||||
assertIsManaged()
|
||||
internal fun RoomEntity.addSendingEvent(realm: Realm, event: Event) {
|
||||
val senderId = event.senderId ?: return
|
||||
val eventEntity = event.toEntity(roomId).apply {
|
||||
this.sendState = SendState.UNSENT
|
||||
|
@ -72,3 +72,4 @@ internal fun RoomEntity.addSendingEvent(event: Event) {
|
|||
}
|
||||
sendingTimelineEvents.add(0, timelineEventEntity)
|
||||
}
|
||||
|
||||
|
|
|
@ -43,9 +43,12 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
|
|||
senderName = timelineEventEntity.senderName,
|
||||
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
||||
senderAvatar = timelineEventEntity.senderAvatar,
|
||||
readReceipts = readReceipts?.sortedByDescending {
|
||||
it.originServerTs
|
||||
} ?: emptyList()
|
||||
readReceipts = readReceipts
|
||||
?.distinctBy {
|
||||
it.user
|
||||
}?.sortedByDescending {
|
||||
it.originServerTs
|
||||
} ?: emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.GroupUsers
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -53,12 +54,12 @@ internal class DefaultGetGroupDataTask @Inject constructor(
|
|||
insertInDb(groupSummary, groupRooms, groupUsers, groupId)
|
||||
}
|
||||
|
||||
private fun insertInDb(groupSummary: GroupSummaryResponse,
|
||||
private suspend fun insertInDb(groupSummary: GroupSummaryResponse,
|
||||
groupRooms: GroupRooms,
|
||||
groupUsers: GroupUsers,
|
||||
groupId: String) {
|
||||
monarchy
|
||||
.writeAsync { realm ->
|
||||
.awaitTransaction { realm ->
|
||||
val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst()
|
||||
?: realm.createObject(GroupSummaryEntity::class.java, groupId)
|
||||
|
||||
|
|
|
@ -50,25 +50,25 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
|||
private val typingServiceFactory: DefaultTypingService.Factory,
|
||||
private val relationServiceFactory: DefaultRelationService.Factory,
|
||||
private val membershipServiceFactory: DefaultMembershipService.Factory,
|
||||
private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory) :
|
||||
private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory):
|
||||
RoomFactory {
|
||||
|
||||
override fun create(roomId: String): Room {
|
||||
return DefaultRoom(
|
||||
roomId,
|
||||
monarchy,
|
||||
roomSummaryMapper,
|
||||
timelineServiceFactory.create(roomId),
|
||||
sendServiceFactory.create(roomId),
|
||||
draftServiceFactory.create(roomId),
|
||||
stateServiceFactory.create(roomId),
|
||||
reportingServiceFactory.create(roomId),
|
||||
readServiceFactory.create(roomId),
|
||||
typingServiceFactory.create(roomId),
|
||||
cryptoService,
|
||||
relationServiceFactory.create(roomId),
|
||||
membershipServiceFactory.create(roomId),
|
||||
roomPushRuleServiceFactory.create(roomId)
|
||||
roomId = roomId,
|
||||
monarchy = monarchy,
|
||||
roomSummaryMapper = roomSummaryMapper,
|
||||
timelineService = timelineServiceFactory.create(roomId),
|
||||
sendService = sendServiceFactory.create(roomId),
|
||||
draftService = draftServiceFactory.create(roomId),
|
||||
stateService = stateServiceFactory.create(roomId),
|
||||
reportingService = reportingServiceFactory.create(roomId),
|
||||
readService = readServiceFactory.create(roomId),
|
||||
typingService = typingServiceFactory.create(roomId),
|
||||
cryptoService = cryptoService,
|
||||
relationService = relationServiceFactory.create(roomId),
|
||||
roomMembersService = membershipServiceFactory.create(roomId),
|
||||
roomPushRuleService = roomPushRuleServiceFactory.create(roomId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,23 +17,21 @@
|
|||
package im.vector.matrix.android.internal.session.room.draft
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
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.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 io.realm.kotlin.createObject
|
||||
import timber.log.Timber
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.launchToCallback
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
|
||||
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 {
|
||||
|
||||
@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,
|
||||
* or even move an existing draft to the top of the list
|
||||
*/
|
||||
override fun saveDraft(draft: UserDraft) {
|
||||
Timber.d("Draft: saveDraft ${privacySafe(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
draftRepository.saveDraft(roomId, draft)
|
||||
}
|
||||
}
|
||||
|
||||
private fun privacySafe(o: Any): Any {
|
||||
if (BuildConfig.LOG_PRIVATE_DATA) {
|
||||
return o
|
||||
}
|
||||
|
||||
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 deleteDraft(callback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
draftRepository.deleteDraft(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDraftsLive(): 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()
|
||||
}
|
||||
return draftRepository.getDraftsLive(roomId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ""
|
||||
}
|
||||
}
|
|
@ -242,6 +242,6 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||
* the same transaction id is received (in unsigned data)
|
||||
*/
|
||||
private fun saveLocalEcho(event: Event) {
|
||||
eventFactory.saveLocalEcho(monarchy, event)
|
||||
eventFactory.createLocalEcho(event)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,35 +17,35 @@
|
|||
package im.vector.matrix.android.internal.session.room.send
|
||||
|
||||
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.AssistedInject
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
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.events.model.*
|
||||
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.events.model.Event
|
||||
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.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.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.session.content.UploadContentWorker
|
||||
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.worker.AlwaysSuccessfulWorker
|
||||
import im.vector.matrix.android.internal.worker.WorkManagerUtil
|
||||
import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.worker.startChain
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -59,7 +59,9 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
@SessionId private val sessionId: String,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val cryptoService: CryptoService,
|
||||
private val monarchy: Monarchy
|
||||
private val monarchy: Monarchy,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val localEchoRepository: LocalEchoRepository
|
||||
) : SendService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
|
@ -71,15 +73,14 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
|
||||
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
|
||||
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
|
||||
saveLocalEcho(it)
|
||||
createLocalEcho(it)
|
||||
}
|
||||
|
||||
return sendEvent(event)
|
||||
}
|
||||
|
||||
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
|
||||
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also {
|
||||
saveLocalEcho(it)
|
||||
createLocalEcho(it)
|
||||
}
|
||||
|
||||
return sendEvent(event)
|
||||
|
@ -157,13 +158,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun deleteFailedEcho(localEcho: TimelineEvent) {
|
||||
monarchy.writeAsync { realm ->
|
||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.let {
|
||||
it.deleteFromRealm()
|
||||
}
|
||||
EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let {
|
||||
it.deleteFromRealm()
|
||||
}
|
||||
taskExecutor.executorScope.launch {
|
||||
localEchoRepository.deleteFailedEcho(roomId, localEcho)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,67 +177,26 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it)
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
monarchy.writeAsync { realm ->
|
||||
RoomEntity.where(realm, roomId).findFirst()?.let { room ->
|
||||
room.sendingTimelineEvents.forEach {
|
||||
it.root?.sendState = SendState.UNDELIVERED
|
||||
}
|
||||
}
|
||||
taskExecutor.executorScope.launch {
|
||||
localEchoRepository.clearSendingQueue(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resendAllFailedMessages() {
|
||||
monarchy.writeAsync { realm ->
|
||||
TimelineEventEntity
|
||||
.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES)
|
||||
.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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
taskExecutor.executorScope.launch {
|
||||
val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId)
|
||||
eventsToResend.forEach {
|
||||
sendEvent(it)
|
||||
}
|
||||
localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
|
||||
// Create an event with the media file path
|
||||
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
|
||||
saveLocalEcho(it)
|
||||
createLocalEcho(it)
|
||||
}
|
||||
|
||||
return internalSendMedia(event, attachment)
|
||||
}
|
||||
|
||||
|
@ -276,8 +231,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
return CancelableWork(context, sendWork.id)
|
||||
}
|
||||
|
||||
private fun saveLocalEcho(event: Event) {
|
||||
localEchoEventFactory.saveLocalEcho(monarchy, event)
|
||||
private fun createLocalEcho(event: Event) {
|
||||
localEchoEventFactory.createLocalEcho(event)
|
||||
}
|
||||
|
||||
private fun buildWorkName(identifier: String): String {
|
||||
|
@ -306,7 +261,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
|
||||
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
|
||||
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 redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
|
|
@ -18,26 +18,42 @@ package im.vector.matrix.android.internal.session.room.send
|
|||
|
||||
import android.media.MediaMetadataRetriever
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.R
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||
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.room.model.message.*
|
||||
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.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.ReactionInfo
|
||||
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.timeline.TimelineEvent
|
||||
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.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
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.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.util.StringProvider
|
||||
import kotlinx.coroutines.launch
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import javax.inject.Inject
|
||||
|
@ -54,8 +70,9 @@ import javax.inject.Inject
|
|||
internal class LocalEchoEventFactory @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
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
|
||||
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" }
|
||||
monarchy.writeAsync { realm ->
|
||||
val roomEntity = RoomEntity.where(realm, roomId = event.roomId).findFirst()
|
||||
?: return@writeAsync
|
||||
roomEntity.addSendingEvent(event)
|
||||
roomSummaryUpdater.update(realm, event.roomId)
|
||||
taskExecutor.executorScope.launch {
|
||||
localEchoRepository.createLocalEcho(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -50,6 +50,9 @@ import io.realm.RealmConfiguration
|
|||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import io.realm.Sort
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import timber.log.Timber
|
||||
import java.util.Collections
|
||||
import java.util.UUID
|
||||
|
@ -72,9 +75,12 @@ internal class DefaultTimeline(
|
|||
private val cryptoService: CryptoService,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val settings: TimelineSettings,
|
||||
private val hiddenReadReceipts: TimelineHiddenReadReceipts
|
||||
private val hiddenReadReceipts: TimelineHiddenReadReceipts,
|
||||
private val eventBus: EventBus
|
||||
) : Timeline, TimelineHiddenReadReceipts.Delegate {
|
||||
|
||||
data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>)
|
||||
|
||||
companion object {
|
||||
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
|
||||
}
|
||||
|
@ -128,7 +134,7 @@ internal class DefaultTimeline(
|
|||
if (hasChange) postSnapshot()
|
||||
}
|
||||
|
||||
// Public methods ******************************************************************************
|
||||
// Public methods ******************************************************************************
|
||||
|
||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||
BACKGROUND_HANDLER.post {
|
||||
|
@ -159,6 +165,7 @@ internal class DefaultTimeline(
|
|||
override fun start() {
|
||||
if (isStarted.compareAndSet(false, true)) {
|
||||
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
|
||||
eventBus.register(this)
|
||||
BACKGROUND_HANDLER.post {
|
||||
eventDecryptor.start()
|
||||
val realm = Realm.getInstance(realmConfiguration)
|
||||
|
@ -190,12 +197,13 @@ internal class DefaultTimeline(
|
|||
}
|
||||
|
||||
private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean {
|
||||
return settings.buildReadReceipts && (settings.filterEdits || settings.filterTypes)
|
||||
return buildReadReceipts && (filterEdits || filterTypes)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
if (isStarted.compareAndSet(true, false)) {
|
||||
isReady.set(false)
|
||||
eventBus.unregister(this)
|
||||
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
|
||||
cancelableBag.cancel()
|
||||
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
|
||||
|
@ -316,6 +324,15 @@ internal class DefaultTimeline(
|
|||
postSnapshot()
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) {
|
||||
if (onNewTimelineEvents.roomId == roomId) {
|
||||
listeners.forEach {
|
||||
it.onNewTimelineEvents(onNewTimelineEvents.eventIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Private methods *****************************************************************************
|
||||
|
||||
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
|
||||
|
@ -401,14 +418,14 @@ internal class DefaultTimeline(
|
|||
|
||||
private fun getState(direction: Timeline.Direction): State {
|
||||
return when (direction) {
|
||||
Timeline.Direction.FORWARDS -> forwardsState.get()
|
||||
Timeline.Direction.FORWARDS -> forwardsState.get()
|
||||
Timeline.Direction.BACKWARDS -> backwardsState.get()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateState(direction: Timeline.Direction, update: (State) -> State) {
|
||||
val stateReference = when (direction) {
|
||||
Timeline.Direction.FORWARDS -> forwardsState
|
||||
Timeline.Direction.FORWARDS -> forwardsState
|
||||
Timeline.Direction.BACKWARDS -> backwardsState
|
||||
}
|
||||
val currentValue = stateReference.get()
|
||||
|
@ -506,10 +523,10 @@ internal class DefaultTimeline(
|
|||
this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
||||
when (data) {
|
||||
TokenChunkEventPersistor.Result.SUCCESS -> {
|
||||
TokenChunkEventPersistor.Result.SUCCESS -> {
|
||||
Timber.v("Success fetching $limit items $direction from pagination request")
|
||||
}
|
||||
TokenChunkEventPersistor.Result.REACHED_END -> {
|
||||
TokenChunkEventPersistor.Result.REACHED_END -> {
|
||||
postSnapshot()
|
||||
}
|
||||
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
|
||||
|
|
|
@ -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.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.util.fetchCopyMap
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val monarchy: Monarchy,
|
||||
private val eventBus: EventBus,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val contextOfEventTask: GetContextOfEventTask,
|
||||
private val cryptoService: CryptoService,
|
||||
|
@ -52,17 +54,19 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
|||
}
|
||||
|
||||
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
|
||||
return DefaultTimeline(roomId,
|
||||
eventId,
|
||||
monarchy.realmConfiguration,
|
||||
taskExecutor,
|
||||
contextOfEventTask,
|
||||
clearUnlinkedEventsTask,
|
||||
paginationTask,
|
||||
cryptoService,
|
||||
timelineEventMapper,
|
||||
settings,
|
||||
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings)
|
||||
return DefaultTimeline(
|
||||
roomId = roomId,
|
||||
initialEventId = eventId,
|
||||
realmConfiguration = monarchy.realmConfiguration,
|
||||
taskExecutor = taskExecutor,
|
||||
contextOfEventTask = contextOfEventTask,
|
||||
clearUnlinkedEventsTask = clearUnlinkedEventsTask,
|
||||
paginationTask = paginationTask,
|
||||
cryptoService = cryptoService,
|
||||
timelineEventMapper = timelineEventMapper,
|
||||
settings = settings,
|
||||
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
|
||||
eventBus = eventBus
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.membership.RoomMemberEventHandler
|
||||
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.typing.TypingEventContent
|
||||
import im.vector.matrix.android.internal.session.sync.model.*
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -50,7 +52,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
private val roomFullyReadHandler: RoomFullyReadHandler,
|
||||
private val cryptoService: DefaultCryptoService,
|
||||
private val roomMemberEventHandler: RoomMemberEventHandler,
|
||||
private val timelineEventSenderVisitor: TimelineEventSenderVisitor) {
|
||||
private val timelineEventSenderVisitor: TimelineEventSenderVisitor,
|
||||
private val eventBus: EventBus) {
|
||||
|
||||
sealed class 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) {
|
||||
val chunkEntity = handleTimelineEvents(
|
||||
realm,
|
||||
roomId,
|
||||
roomEntity,
|
||||
roomSync.timeline.events,
|
||||
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)
|
||||
roomEntity.membership = Membership.INVITE
|
||||
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)
|
||||
}
|
||||
val hasRoomMember = roomSync.inviteState?.events?.firstOrNull {
|
||||
|
@ -183,6 +187,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
}
|
||||
|
||||
private fun handleTimelineEvents(realm: Realm,
|
||||
roomId: String,
|
||||
roomEntity: RoomEntity,
|
||||
eventList: List<Event>,
|
||||
prevToken: String? = null,
|
||||
|
@ -202,7 +207,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
chunkEntity.isUnlinked = false
|
||||
|
||||
val timelineEvents = ArrayList<TimelineEventEntity>(eventList.size)
|
||||
val eventIds = ArrayList<String>(eventList.size)
|
||||
for (event in eventList) {
|
||||
if(event.eventId != null) {
|
||||
eventIds.add(event.eventId)
|
||||
}
|
||||
chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)?.also {
|
||||
timelineEvents.add(it)
|
||||
}
|
||||
|
@ -221,6 +230,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
roomMemberEventHandler.handle(realm, roomEntity.roomId, event)
|
||||
}
|
||||
timelineEventSenderVisitor.visit(timelineEvents)
|
||||
// posting new events to timeline if any is registered
|
||||
eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = eventIds))
|
||||
return chunkEntity
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ import kotlin.coroutines.EmptyCoroutineContext
|
|||
@MatrixScope
|
||||
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 {
|
||||
return executorScope
|
||||
|
|
|
@ -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>
|
|
@ -312,6 +312,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
.subscribe {
|
||||
when (it) {
|
||||
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
|
||||
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
|
||||
}
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
|
|
@ -21,4 +21,5 @@ package im.vector.riotx.features.home.room.detail
|
|||
*/
|
||||
sealed class RoomDetailViewEvents {
|
||||
data class Failure(val throwable: Throwable) : RoomDetailViewEvents()
|
||||
data class OnNewTimelineEvents(val eventIds: List<String>) : RoomDetailViewEvents()
|
||||
}
|
||||
|
|
|
@ -20,7 +20,12 @@ import android.net.Uri
|
|||
import androidx.annotation.IdRes
|
||||
import androidx.lifecycle.LiveData
|
||||
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.PublishRelay
|
||||
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.utils.DataSource
|
||||
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.subscribeLogError
|
||||
import im.vector.riotx.features.command.CommandParser
|
||||
|
@ -218,10 +224,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) {
|
||||
withState {
|
||||
when (it.sendMode) {
|
||||
is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft))
|
||||
is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, 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), NoOpMatrixCallback())
|
||||
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), NoOpMatrixCallback())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -465,7 +471,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
}
|
||||
|
||||
private fun popDraft() {
|
||||
room.deleteDraft()
|
||||
room.deleteDraft(object : MatrixCallback<Unit> {})
|
||||
}
|
||||
|
||||
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 ->
|
||||
setState { copy(sendMode = SendMode.EDIT(timelineEvent, action.text)) }
|
||||
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
|
||||
timelineEvent.root.eventId?.let {
|
||||
if (state.sendMode is SendMode.EDIT) {
|
||||
room.saveDraft(UserDraft.QUOTE(it, ""))
|
||||
room.saveDraft(UserDraft.QUOTE(it, ""), NoOpMatrixCallback())
|
||||
} 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
|
||||
timelineEvent.root.eventId?.let {
|
||||
if (state.sendMode is SendMode.EDIT) {
|
||||
room.saveDraft(UserDraft.REPLY(it, ""))
|
||||
room.saveDraft(UserDraft.REPLY(it, ""), NoOpMatrixCallback())
|
||||
} 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 {
|
||||
if (draft.isNotBlank()) {
|
||||
when (it.sendMode) {
|
||||
is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft))
|
||||
is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, draft))
|
||||
is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, draft))
|
||||
is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, draft))
|
||||
is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft), NoOpMatrixCallback())
|
||||
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), NoOpMatrixCallback())
|
||||
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 ->
|
||||
// For edit, just delete the current draft
|
||||
if (state.sendMode is SendMode.EDIT) {
|
||||
room.deleteDraft()
|
||||
room.deleteDraft(NoOpMatrixCallback())
|
||||
} else {
|
||||
// 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))
|
||||
}
|
||||
|
||||
override fun onNewTimelineEvents(eventIds: List<String>) {
|
||||
Timber.v("On new timeline events: $eventIds")
|
||||
_viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds))
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
timeline.dispose()
|
||||
timeline.removeAllListeners()
|
||||
|
|
|
@ -19,15 +19,29 @@ package im.vector.riotx.features.home.room.detail
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.item.BaseEventItem
|
||||
import timber.log.Timber
|
||||
|
||||
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
|
||||
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) {
|
||||
Timber.v("On inserted $count count at position: $position")
|
||||
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) {
|
||||
layoutManager.scrollToPosition(0)
|
||||
if(layoutManager.findFirstVisibleItemPosition() != position ){
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.riotx.core.date.VectorDateFormatter
|
||||
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.features.home.room.detail.RoomDetailViewState
|
||||
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
|
||||
}
|
||||
|
||||
override fun onNewTimelineEvents(eventIds: List<String>) {
|
||||
// no-op, already handled
|
||||
}
|
||||
|
||||
private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
|
||||
backgroundHandler.post {
|
||||
inSubmitList = true
|
||||
|
@ -346,8 +351,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
return positionOfReadMarker
|
||||
}
|
||||
|
||||
fun isLoadingForward() = showingForwardLoader
|
||||
|
||||
private data class CacheItemData(
|
||||
val localId: Long,
|
||||
val eventId: String?,
|
||||
|
|
Loading…
Reference in New Issue