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
|
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
|
||||||
|
@ -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>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
* 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.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 ->
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
.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()
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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?,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user