From 648c4d263c7cfafb5648c5b4f38c665d8cf18bbf Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Fri, 14 Jul 2023 17:47:27 +0200 Subject: [PATCH 1/2] creating LastEdit --- .../storyteller/model/story/LastEdit.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/model/story/LastEdit.kt diff --git a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/model/story/LastEdit.kt b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/model/story/LastEdit.kt new file mode 100644 index 000000000..dbb302a05 --- /dev/null +++ b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/model/story/LastEdit.kt @@ -0,0 +1,29 @@ +package com.github.leandroborgesferreira.storyteller.model.story + +import com.github.leandroborgesferreira.storyteller.model.document.Document + +/** + * Last edition. This signs how the last edition in the [Document] was made and it allows the SDK + * to react properly, for example saving just one line of change instead saving the whole document. + */ +sealed class LastEdit { + /** + * No edition was make + */ + object Nothing + + /** + * A whole edition was made like a line break (adding a new line) , a deletion + * o a image upload between content. + * In this case the whole document should be saved. + * It is important to notice that when new content is added between content this will change all + * the values of the positions, so it is necessary to save the whole document again. + */ + object WholeEdition + + /** + * A edition in the line was made, but the positions were not affected. In this case it is + * possible to update just one line. + */ + data class LineEdition(val position: Int, val text: String) +} \ No newline at end of file From ffd0a45b60ed0c80f8041636d98e9d187f3e7faf Mon Sep 17 00:00:00 2001 From: Leandro Borges Ferreira Date: Fri, 14 Jul 2023 20:18:42 +0200 Subject: [PATCH 2/2] Editing only a line when the LastEdit was partial --- .../storyteller/manager/ContentHandler.kt | 8 +- .../storyteller/manager/DocumentRepository.kt | 4 +- .../{SaveNote.kt => DocumentUpdate.kt} | 5 +- .../storyteller/manager/StoryTellerManager.kt | 130 ++++++++++++------ .../storyteller/model/story/LastEdit.kt | 6 +- .../storyteller/model/story/StoryState.kt | 1 + .../repository/DocumentRepositoryImpl.kt | 4 + 7 files changed, 109 insertions(+), 49 deletions(-) rename storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/{SaveNote.kt => DocumentUpdate.kt} (50%) diff --git a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/ContentHandler.kt b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/ContentHandler.kt index d9271ac36..a1f368d69 100644 --- a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/ContentHandler.kt +++ b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/ContentHandler.kt @@ -2,6 +2,7 @@ package com.github.leandroborgesferreira.storyteller.manager import com.github.leandroborgesferreira.storyteller.model.change.DeleteInfo import com.github.leandroborgesferreira.storyteller.model.change.LineBreakInfo +import com.github.leandroborgesferreira.storyteller.model.story.LastEdit import com.github.leandroborgesferreira.storyteller.model.story.StoryState import com.github.leandroborgesferreira.storyteller.model.story.StoryStep import com.github.leandroborgesferreira.storyteller.model.story.StoryType @@ -35,7 +36,7 @@ class ContentHandler( ) newMap[position] = newCheck - return StoryState(newMap, newCheck.id) + return StoryState(newMap, LastEdit.Whole, newCheck.id) } //Todo: Add unit test @@ -93,6 +94,7 @@ class ContentHandler( (addPosition to secondMessage) to StoryState( stories = newStory, + lastEdit = LastEdit.Whole, focusId = secondMessage.id ) } @@ -113,7 +115,7 @@ class ContentHandler( ) val normalized = stepsNormalizer(mutableSteps.toEditState()) - StoryState(normalized, focusId = previousFocus?.id) + StoryState(normalized, lastEdit = LastEdit.Whole, focusId = previousFocus?.id) } else { mutableSteps[deleteInfo.position]?.let { group -> val newSteps = group.steps.filter { storyUnit -> @@ -127,7 +129,7 @@ class ContentHandler( } mutableSteps[deleteInfo.position] = newStoryUnit.copy(parentId = null) - StoryState(stepsNormalizer(mutableSteps.toEditState())) + StoryState(stepsNormalizer(mutableSteps.toEditState()), lastEdit = LastEdit.Whole) } } } diff --git a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/DocumentRepository.kt b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/DocumentRepository.kt index 3d881ec85..360ac0658 100644 --- a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/DocumentRepository.kt +++ b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/DocumentRepository.kt @@ -8,7 +8,7 @@ import com.github.leandroborgesferreira.storyteller.model.story.StoryStep * The implementations of this interface shouldn't control order (sorting) or oder configurations, * those need to be passed as parameters. */ -interface DocumentRepository : SaveNote { +interface DocumentRepository : DocumentUpdate { suspend fun loadDocuments(orderBy: String): List @@ -18,6 +18,8 @@ interface DocumentRepository : SaveNote { override suspend fun saveDocument(document: Document) + override suspend fun saveStoryStep(storyStep: StoryStep, position: Int, documentId: String) + suspend fun deleteDocument(document: Document) suspend fun deleteDocumentById(ids: Set) diff --git a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/SaveNote.kt b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/DocumentUpdate.kt similarity index 50% rename from storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/SaveNote.kt rename to storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/DocumentUpdate.kt index d19ee6ab6..3e196c4d3 100644 --- a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/SaveNote.kt +++ b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/DocumentUpdate.kt @@ -1,7 +1,10 @@ package com.github.leandroborgesferreira.storyteller.manager import com.github.leandroborgesferreira.storyteller.model.document.Document +import com.github.leandroborgesferreira.storyteller.model.story.StoryStep -interface SaveNote { +interface DocumentUpdate { suspend fun saveDocument(document: Document) + + suspend fun saveStoryStep(storyStep: StoryStep, position: Int, documentId: String) } diff --git a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/StoryTellerManager.kt b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/StoryTellerManager.kt index 5ca9e8c2c..fffe2fe53 100644 --- a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/StoryTellerManager.kt +++ b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/manager/StoryTellerManager.kt @@ -15,6 +15,7 @@ import com.github.leandroborgesferreira.storyteller.model.change.TextEditInfo import com.github.leandroborgesferreira.storyteller.model.document.Document import com.github.leandroborgesferreira.storyteller.model.story.DrawState import com.github.leandroborgesferreira.storyteller.model.story.DrawStory +import com.github.leandroborgesferreira.storyteller.model.story.LastEdit import com.github.leandroborgesferreira.storyteller.model.story.StoryState import com.github.leandroborgesferreira.storyteller.model.story.StoryStep import com.github.leandroborgesferreira.storyteller.model.story.StoryType @@ -24,15 +25,13 @@ import com.github.leandroborgesferreira.storyteller.utils.extensions.toEditState import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import java.util.Date import java.util.UUID @@ -63,7 +62,7 @@ class StoryTellerManager( val scrollToPosition: StateFlow = _scrollToPosition.asStateFlow() private val _currentStory: MutableStateFlow = MutableStateFlow( - StoryState(stories = emptyMap()) + StoryState(stories = emptyMap(), lastEdit = LastEdit.Nothing) ) private val _documentInfo: MutableStateFlow = MutableStateFlow(DocumentInfo()) @@ -73,45 +72,73 @@ class StoryTellerManager( val currentStory: StateFlow = _currentStory.asStateFlow() - @OptIn(ExperimentalCoroutinesApi::class) - val currentDocument: Flow = _documentInfo.flatMapLatest { info -> - _currentStory.map { state -> - val titleFromContent = state.stories.values.firstOrNull { storyStep -> - //Todo: Change the type of change to allow different types. The client code should decide what is a title - //It is also interesting to inv - storyStep.type == StoryType.TITLE.type - }?.text - - Document( - id = info.id, - title = titleFromContent ?: info.title, - content = state.stories, - createdAt = info.createdAt, - lastUpdatedAt = info.lastUpdatedAt, - ) - } + val currentDocument: Flow = combine(_documentInfo, _currentStory) { info, state -> + val titleFromContent = state.stories.values.firstOrNull { storyStep -> + //Todo: Change the type of change to allow different types. The client code should decide what is a title + //It is also interesting to inv + storyStep.type == StoryType.TITLE.type + }?.text + + Document( + id = info.id, + title = titleFromContent ?: info.title, + content = state.stories, + createdAt = info.createdAt, + lastUpdatedAt = info.lastUpdatedAt, + ) } - @OptIn(ExperimentalCoroutinesApi::class) - val toDraw = _positionsOnEdit.flatMapLatest { positions -> - currentStory.map { storyState -> - val focus = storyState.focusId + private val _documentEditionState: Flow> = + combine(currentStory, _documentInfo) { storyState, documentInfo -> + storyState to documentInfo + } - val toDrawStories = storyState.stories.mapValues { (position, storyStep) -> - DrawStory(storyStep, positions.contains(position)) - } + val toDraw = combine(_positionsOnEdit, currentStory) { positions, storyState -> + val focus = storyState.focusId - DrawState(toDrawStories, focus) + val toDrawStories = storyState.stories.mapValues { (position, storyStep) -> + DrawStory(storyStep, positions.contains(position)) } + + DrawState(toDrawStories, focus) } private val isOnSelection: Boolean get() = _positionsOnEdit.value.isNotEmpty() - fun saveOnStoryChanges(saveNote: SaveNote) { + fun saveOnStoryChanges(documentUpdate: DocumentUpdate) { coroutineScope.launch(dispatcher) { - currentDocument.collectLatest { document -> - saveNote.saveDocument(document) + _documentEditionState.collectLatest { (storyState, documentInfo) -> + when (val lastEdit = storyState.lastEdit) { + is LastEdit.LineEdition -> { + documentUpdate.saveStoryStep( + storyStep = lastEdit.storyStep, + position = lastEdit.position, + documentId = documentInfo.id + ) + } + + LastEdit.Nothing -> {} + + LastEdit.Whole -> { + val stories = storyState.stories + val titleFromContent = stories.values.firstOrNull { storyStep -> + //Todo: Change the type of change to allow different types. The client code should decide what is a title + //It is also interesting to inv + storyStep.type == StoryType.TITLE.type + }?.text + + val document = Document( + id = documentInfo.id, + title = titleFromContent ?: documentInfo.title, + content = storyState.stories, + createdAt = documentInfo.createdAt, + lastUpdatedAt = documentInfo.lastUpdatedAt, + ) + + documentUpdate.saveDocument(document) + } + } } } } @@ -135,6 +162,7 @@ class StoryTellerManager( _currentStory.value = StoryState( normalized + normalized, + LastEdit.Nothing, firstMessage.id ) } @@ -158,10 +186,11 @@ class StoryTellerManager( if (isInitialized()) return val stories = document.content ?: emptyMap() - _currentStory.value = StoryState(stepsNormalizer(stories.toEditState()), null) + _currentStory.value = + StoryState(stepsNormalizer(stories.toEditState()), LastEdit.Nothing, null) val normalized = stepsNormalizer(stories.toEditState()) - _currentStory.value = StoryState(normalized) + _currentStory.value = StoryState(normalized, LastEdit.Nothing) _documentInfo.value = document.info() } @@ -171,7 +200,10 @@ class StoryTellerManager( } val movedStories = movementHandler.merge(_currentStory.value.stories, info) - _currentStory.value = StoryState(stories = stepsNormalizer(movedStories)) + _currentStory.value = StoryState( + stories = stepsNormalizer(movedStories), + lastEdit = LastEdit.Whole + ) } fun moveRequest(moveInfo: MoveInfo) { @@ -180,7 +212,10 @@ class StoryTellerManager( } val newStory = movementHandler.move(_currentStory.value.stories, moveInfo) - _currentStory.value = StoryState(stepsNormalizer(newStory.toEditState())) + _currentStory.value = StoryState( + stepsNormalizer(newStory.toEditState()), + lastEdit = LastEdit.Whole + ) } /** @@ -264,7 +299,7 @@ class StoryTellerManager( acc to StoryStep(type = StoryType.LARGE_SPACE.type), ) - _currentStory.value = StoryState(newStories, newLastMessage.id) + _currentStory.value = StoryState(newStories, LastEdit.Whole, newLastMessage.id) } } @@ -289,7 +324,10 @@ class StoryTellerManager( StoryStep(type = StoryType.SPACE.type) } ).let { newStories -> - StoryState(stepsNormalizer(newStories.toEditState())) + StoryState( + stepsNormalizer(newStories.toEditState()), + lastEdit = LastEdit.Whole + ) } _currentStory.value = newState @@ -319,6 +357,7 @@ class StoryTellerManager( ) _currentStory.value = StoryState( newStory, + LastEdit.Whole, newStory[action.position]?.id ) @@ -362,7 +401,7 @@ class StoryTellerManager( private fun updateStory(position: Int, newStep: StoryStep, focusId: String? = null) { val newMap = _currentStory.value.stories.toMutableMap() newMap[position] = newStep - _currentStory.value = StoryState(newMap, focusId) + _currentStory.value = StoryState(newMap, LastEdit.LineEdition(position, newStep), focusId) } private fun revertAddText(currentStory: Map, addText: AddText) { @@ -380,7 +419,11 @@ class StoryTellerManager( mutableSteps[addText.position] = revertStep.copy(localId = UUID.randomUUID().toString(), text = newText) - _currentStory.value = StoryState(mutableSteps, focusId = revertStep.id) + _currentStory.value = StoryState( + mutableSteps, + lastEdit = LastEdit.Whole, + focusId = revertStep.id + ) } } @@ -390,7 +433,11 @@ class StoryTellerManager( mutableSteps[position]?.let { editStep -> val newText = "${editStep.text.toString()}${addText.text}" mutableSteps[position] = editStep.copy(text = newText) - _currentStory.value = StoryState(mutableSteps, focusId = editStep.id) + _currentStory.value = StoryState( + mutableSteps, + lastEdit = LastEdit.Whole, + focusId = editStep.id + ) } } @@ -414,6 +461,7 @@ class StoryTellerManager( return StoryState( stories = newStory, + lastEdit = LastEdit.Whole, focusId = deleteInfo.storyUnit.id ) } diff --git a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/model/story/LastEdit.kt b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/model/story/LastEdit.kt index dbb302a05..69f011323 100644 --- a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/model/story/LastEdit.kt +++ b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/model/story/LastEdit.kt @@ -10,7 +10,7 @@ sealed class LastEdit { /** * No edition was make */ - object Nothing + object Nothing: LastEdit() /** * A whole edition was made like a line break (adding a new line) , a deletion @@ -19,11 +19,11 @@ sealed class LastEdit { * It is important to notice that when new content is added between content this will change all * the values of the positions, so it is necessary to save the whole document again. */ - object WholeEdition + object Whole: LastEdit() /** * A edition in the line was made, but the positions were not affected. In this case it is * possible to update just one line. */ - data class LineEdition(val position: Int, val text: String) + data class LineEdition(val position: Int, val storyStep: StoryStep): LastEdit() } \ No newline at end of file diff --git a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/model/story/StoryState.kt b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/model/story/StoryState.kt index e901c8d21..f653e86a0 100644 --- a/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/model/story/StoryState.kt +++ b/storyteller/src/main/java/com/github/leandroborgesferreira/storyteller/model/story/StoryState.kt @@ -6,5 +6,6 @@ package com.github.leandroborgesferreira.storyteller.model.story */ data class StoryState( val stories: Map, + val lastEdit: LastEdit, val focusId: String? = null ) diff --git a/storyteller_persistence/src/main/java/com/github/leandroborgesferreira/storyteller/persistence/repository/DocumentRepositoryImpl.kt b/storyteller_persistence/src/main/java/com/github/leandroborgesferreira/storyteller/persistence/repository/DocumentRepositoryImpl.kt index 55cb8ea29..e0f9ec143 100644 --- a/storyteller_persistence/src/main/java/com/github/leandroborgesferreira/storyteller/persistence/repository/DocumentRepositoryImpl.kt +++ b/storyteller_persistence/src/main/java/com/github/leandroborgesferreira/storyteller/persistence/repository/DocumentRepositoryImpl.kt @@ -56,6 +56,10 @@ class DocumentRepositoryImpl( documentDao.deleteDocuments(*ids.map(DocumentEntity::createById).toTypedArray()) } + override suspend fun saveStoryStep(storyStep: StoryStep, position: Int, documentId: String) { + storyUnitDao.insertStoryUnits(storyStep.toEntity(position, documentId)) + } + /** * This method removes the story units that are not in the root level (they don't have parents) * and loads the inner steps of the steps that have children.