Start reworking draft (simplify)

This commit is contained in:
ganfra 2020-09-22 18:56:05 +02:00 committed by Benoit Marty
parent f030e098a8
commit aa0520d47d
5 changed files with 65 additions and 132 deletions

View file

@ -101,8 +101,11 @@ class RxRoom(private val room: Room) {
return room.getEventReadReceiptsLive(eventId).asObservable()
}
fun liveDrafts(): Observable<List<UserDraft>> {
return room.getDraftsLive().asObservable()
fun liveDraft(): Observable<Optional<UserDraft>> {
return room.getDraftLive().asObservable()
.startWithCallable {
room.getDraft().toOptional()
}
}
fun liveNotificationState(): Observable<RoomNotificationState> {

View file

@ -20,6 +20,7 @@ package org.matrix.android.sdk.api.session.room.send
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
interface DraftService {
@ -34,8 +35,12 @@ interface DraftService {
fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable
/**
* Return the current drafts if any, as a live data
* The draft list can contain one draft for {regular, reply, quote} and an arbitrary number of {edit} drafts
* Return the current draft or null
*/
fun getDraftsLive(): LiveData<List<UserDraft>>
fun getDraft(): UserDraft?
/**
* Return the current draft if any, as a live data
*/
fun getDraftLive(): LiveData<Optional<UserDraft>>
}

View file

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.room.send.DraftService
import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.launchToCallback
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
@ -55,7 +56,11 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private
}
}
override fun getDraftsLive(): LiveData<List<UserDraft>> {
override fun getDraft(): UserDraft? {
return draftRepository.getDraft(roomId)
}
override fun getDraftLive(): LiveData<Optional<UserDraft>> {
return draftRepository.getDraftsLive(roomId)
}
}

View file

@ -20,35 +20,61 @@ package org.matrix.android.sdk.internal.session.room.draft
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.kotlin.createObject
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.mapper.DraftMapper
import org.matrix.android.sdk.internal.database.model.DraftEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.UserDraftsEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.util.awaitTransaction
import io.realm.Realm
import io.realm.kotlin.createObject
import timber.log.Timber
import javax.inject.Inject
class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) {
internal class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) {
suspend fun saveDraft(roomId: String, userDraft: UserDraft) {
monarchy.awaitTransaction {
saveDraft(it, userDraft, roomId)
saveDraftInDb(it, userDraft, roomId)
}
}
suspend fun deleteDraft(roomId: String) {
monarchy.awaitTransaction {
deleteDraft(it, roomId)
deleteDraftFromDb(it, roomId)
}
}
private fun deleteDraft(realm: Realm, roomId: String) {
fun getDraft(roomId: String): UserDraft? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
UserDraftsEntity.where(realm, roomId).findFirst()
?.userDrafts
?.firstOrNull()
?.let {
DraftMapper.map(it)
}
}
}
fun getDraftsLive(roomId: String): LiveData<Optional<UserDraft>> {
val liveData = monarchy.findAllMappedWithChanges(
{ UserDraftsEntity.where(it, roomId) },
{
it.userDrafts.map { draft ->
DraftMapper.map(draft)
}
}
)
return Transformations.map(liveData) {
it.firstOrNull()?.firstOrNull().toOptional()
}
}
private fun deleteDraftFromDb(realm: Realm, roomId: String) {
UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity ->
if (userDraftsEntity.userDrafts.isNotEmpty()) {
userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1)
@ -56,7 +82,7 @@ class DraftRepository @Inject constructor(@SessionDatabase private val monarchy:
}
}
private fun saveDraft(realm: Realm, draft: UserDraft, roomId: String) {
private fun saveDraftInDb(realm: Realm, draft: UserDraft, roomId: String) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
@ -68,62 +94,15 @@ class DraftRepository @Inject constructor(@SessionDatabase private val monarchy:
userDraftsEntity.let { userDraftEntity ->
// Save only valid draft
if (draft.isValid()) {
// Add a new draft or update the current one?
// Replace the current draft
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
}
}
Timber.d("Draft: create a new draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.clear()
userDraftEntity.userDrafts.add(newDraft)
} 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 {
@ -135,20 +114,6 @@ class DraftRepository @Inject constructor(@SessionDatabase private val monarchy:
}
}
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().orEmpty()
}
}
private fun privacySafe(o: Any): Any {
if (BuildConfig.LOG_PRIVATE_DATA) {
return o

View file

@ -680,7 +680,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}.exhaustive
}
is SendMode.EDIT -> {
is SendMode.EDIT -> {
// is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
@ -706,7 +706,7 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
is SendMode.QUOTE -> {
is SendMode.QUOTE -> {
val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
@ -729,7 +729,7 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
is SendMode.REPLY -> {
is SendMode.REPLY -> {
state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
_viewEvents.post(RoomDetailViewEvents.MessageSent)
@ -741,6 +741,9 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun popDraft() {
setState {
copy(sendMode = SendMode.REGULAR(""))
}
room.deleteDraft(NoOpMatrixCallback())
}
@ -915,73 +918,25 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleEditAction(action: RoomDetailAction.EnterEditMode) {
saveCurrentDraft(action.text)
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() ?: ""), NoOpMatrixCallback())
}
setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent() ?: "")) }
}
}
private fun handleQuoteAction(action: RoomDetailAction.EnterQuoteMode) {
saveCurrentDraft(action.text)
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) }
withState { state ->
// 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, ""), NoOpMatrixCallback())
} else {
room.saveDraft(UserDraft.QUOTE(it, action.text), NoOpMatrixCallback())
}
}
}
}
}
private fun handleReplyAction(action: RoomDetailAction.EnterReplyMode) {
saveCurrentDraft(action.text)
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) }
withState { state ->
// 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, ""), NoOpMatrixCallback())
} else {
room.saveDraft(UserDraft.REPLY(it, action.text), NoOpMatrixCallback())
}
}
}
}
}
private fun saveCurrentDraft(draft: String) {
// Save the draft with the current text if any
withState {
if (draft.isNotBlank()) {
when (it.sendMode) {
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())
}
}
}
}
private fun handleExitSpecialMode(action: RoomDetailAction.ExitSpecialMode) = withState {
if (it.sendMode is SendMode.EDIT) {
room.deleteDraft(NoOpMatrixCallback())
} else {
// Save a new draft and keep the previously entered text
room.saveDraft(UserDraft.REGULAR(action.text), NoOpMatrixCallback())
}
room.deleteDraft(NoOpMatrixCallback())
setState { copy(sendMode = SendMode.REGULAR(action.text)) }
}