1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-12 10:40:25 +09:00

DROID-81 Editor | Enhancement | Share files (#2568)

This commit is contained in:
Mikhail 2022-08-30 11:15:19 +03:00 committed by GitHub
parent a64a23e51c
commit 2acfccb6e2
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 408 additions and 79 deletions

View file

@ -194,6 +194,7 @@ import com.anytypeio.anytype.presentation.util.CopyFileStatus
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.OnCopyFileToCacheAction
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
@ -3882,6 +3883,31 @@ class EditorViewModel(
}
}
fun startSharingFile(id: String, onDownloaded: (Uri) -> Unit = {}) {
Timber.d("startDownloadingFile, id:[$id]")
sendToast("Preparing file to share...")
val block = blocks.firstOrNull { it.id == id }
val content = block?.content
if (content is Content.File && content.state == Content.File.State.DONE) {
viewModelScope.launch {
orchestrator.proxies.intents.send(
Media.ShareFile(
hash = content.hash.orEmpty(),
name = content.name.orEmpty(),
type = content.type,
onDownloaded = onDownloaded
)
)
}
} else {
Timber.e("Block is not File or with wrong state, can't proceed with share!")
}
}
fun startDownloadingFile(id: String) {
Timber.d("startDownloadingFile, id:[$id]")
@ -4524,11 +4550,15 @@ class EditorViewModel(
}
}
private fun getObjectTypes(excluded: List<Id> = emptyList(), action: (List<ObjectType>) -> Unit) {
private fun getObjectTypes(
excluded: List<Id> = emptyList(),
action: (List<ObjectType>) -> Unit
) {
viewModelScope.launch {
getCompatibleObjectTypes.invoke(
GetCompatibleObjectTypes.Params(
smartBlockType = blocks.first { it.id == context }.content<Content.Smart>().type,
smartBlockType = blocks.first { it.id == context }
.content<Content.Smart>().type,
excludedTypes = excluded
)
).proceed(
@ -5115,7 +5145,9 @@ class EditorViewModel(
when (val content = selected.content) {
is Content.Bookmark -> {
val target = content.targetObjectId
if (target != null) { proceedWithOpeningPage(target) }
if (target != null) {
proceedWithOpeningPage(target)
}
}
else -> sendToast("Unexpected object")
}

View file

@ -13,7 +13,6 @@ import com.anytypeio.anytype.domain.block.interactor.UpdateLinkMarks
import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet
import com.anytypeio.anytype.domain.cover.SetDocCoverImage
import com.anytypeio.anytype.domain.dataview.interactor.GetCompatibleObjectTypes
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon
import com.anytypeio.anytype.domain.launch.GetDefaultEditorType
@ -21,15 +20,16 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.page.CloseBlock
import com.anytypeio.anytype.domain.page.CreateDocument
import com.anytypeio.anytype.domain.page.CreateNewDocument
import com.anytypeio.anytype.domain.page.CreateNewObject
import com.anytypeio.anytype.domain.page.CreateObject
import com.anytypeio.anytype.domain.page.OpenPage
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.sets.FindObjectSetForType
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.Delegator
import com.anytypeio.anytype.presentation.common.StateReducer
import com.anytypeio.anytype.domain.page.CreateNewObject
import com.anytypeio.anytype.presentation.editor.editor.DetailModificationManager
import com.anytypeio.anytype.presentation.editor.editor.Orchestrator
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableDelegate

View file

@ -1,7 +1,9 @@
package com.anytypeio.anytype.presentation.editor.editor
import android.net.Uri
import android.os.Parcelable
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Hash
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Position
import com.anytypeio.anytype.core_utils.ext.Mimetype
@ -174,6 +176,13 @@ sealed class Intent {
val type: Block.Content.File.Type?
) : Media()
class ShareFile(
val hash: Hash,
val name: String,
val type: Block.Content.File.Type?,
val onDownloaded: (Uri) -> Unit
) : Media()
class Upload(
val context: Id,
val description: Description,

View file

@ -35,6 +35,7 @@ import com.anytypeio.anytype.domain.page.bookmark.CreateBookmarkBlock
import com.anytypeio.anytype.domain.page.bookmark.SetupBookmark
import com.anytypeio.anytype.domain.table.CreateTable
import com.anytypeio.anytype.domain.table.FillTableRow
import com.anytypeio.anytype.presentation.dashboard.HomeDashboardStateMachine
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.extension.sendAnalyticsChangeTextBlockStyleEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsCopyBlockEvent
@ -48,6 +49,7 @@ import com.anytypeio.anytype.presentation.extension.sendAnalyticsReorderBlockEve
import com.anytypeio.anytype.presentation.extension.sendAnalyticsSplitBlockEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsUndoEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsUploadMediaEvent
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
import timber.log.Timber
class Orchestrator(
@ -64,6 +66,7 @@ class Orchestrator(
private val turnIntoStyle: TurnIntoStyle,
private val updateCheckbox: UpdateCheckbox,
private val downloadFile: DownloadFile,
private val middlewareShareDownloader: MiddlewareShareDownloader,
val updateText: UpdateText,
private val updateAlignment: UpdateAlignment,
private val uploadBlock: UploadBlock,
@ -432,6 +435,20 @@ class Orchestrator(
success = { analytics.sendAnalyticsDownloadMediaEvent(intent.type) }
)
}
is Intent.Media.ShareFile -> {
middlewareShareDownloader.execute(
params = MiddlewareShareDownloader.Params(
hash = intent.hash,
name = intent.name
)
).fold(
onSuccess = { uri ->
intent.onDownloaded(uri)
analytics.sendAnalyticsDownloadMediaEvent(intent.type)
},
onFailure = { e -> Timber.e(e, "Error while sharing a file") }
)
}
is Intent.Media.Upload -> {
uploadBlock(
params = UploadBlock.Params(

View file

@ -0,0 +1,55 @@
package com.anytypeio.anytype.presentation.util.downloader
import android.content.Context
import android.net.Uri
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Hash
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.presentation.util.TEMPORARY_DIRECTORY_NAME
import kotlinx.coroutines.withContext
import java.io.File
class MiddlewareShareDownloader(
private val repo: BlockRepository,
private val dispatchers: AppCoroutineDispatchers,
private val context: Context,
private val uriFileProvider: UriFileProvider
) : ResultInteractor<MiddlewareShareDownloader.Params, Uri>() {
data class Params(
val hash: Hash,
val name: String
)
override suspend fun doWork(params: Params) = withContext(dispatchers.io) {
val cacheDir = context.cacheDir
require(cacheDir != null) { "Impossible to cache files!" }
val downloadFolder = File("${cacheDir.path}/${params.hash}").apply { mkdirs() }
val resultFilePath = "${cacheDir.path}/${params.hash}/${params.name}"
val resultFile = File(resultFilePath)
if (!resultFile.exists()) {
val tempFileFolderPath = "${downloadFolder.absolutePath}/tmp"
val tempDir = File(tempFileFolderPath)
if (tempDir.exists()) tempDir.deleteRecursively()
tempDir.mkdirs()
val tempResult = File(
repo.downloadFile(
Command.DownloadFile(
hash = params.hash,
path = tempFileFolderPath
)
)
)
tempResult.renameTo(resultFile)
}
uriFileProvider.getUriForFile(resultFile)
}
}

View file

@ -0,0 +1,10 @@
package com.anytypeio.anytype.presentation.util.downloader
import android.net.Uri
import java.io.File
interface UriFileProvider {
fun getUriForFile(file: File): Uri
}

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.presentation.editor
import android.net.Uri
import android.os.Build
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.anytypeio.anytype.analytics.base.Analytics
@ -13,7 +14,9 @@ import com.anytypeio.anytype.core_models.SmartBlockType
import com.anytypeio.anytype.core_models.StubFile
import com.anytypeio.anytype.core_models.StubNumbered
import com.anytypeio.anytype.core_models.StubParagraph
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_models.ext.content
import com.anytypeio.anytype.core_models.ext.parseThemeTextColor
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import com.anytypeio.anytype.core_utils.common.EventWrapper
import com.anytypeio.anytype.core_utils.ext.Mimetype
@ -50,8 +53,6 @@ import com.anytypeio.anytype.domain.clipboard.Paste
import com.anytypeio.anytype.domain.config.Gateway
import com.anytypeio.anytype.domain.cover.SetDocCoverImage
import com.anytypeio.anytype.domain.dataview.interactor.GetCompatibleObjectTypes
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.relations.SetRelationKey
import com.anytypeio.anytype.domain.download.DownloadFile
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon
@ -68,12 +69,14 @@ import com.anytypeio.anytype.domain.page.Undo
import com.anytypeio.anytype.domain.page.UpdateTitle
import com.anytypeio.anytype.domain.page.bookmark.CreateBookmarkBlock
import com.anytypeio.anytype.domain.page.bookmark.SetupBookmark
import com.anytypeio.anytype.domain.relations.SetRelationKey
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.sets.FindObjectSetForType
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.table.CreateTable
import com.anytypeio.anytype.domain.table.FillTableRow
import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.domain.unsplash.UnsplashRepository
import com.anytypeio.anytype.presentation.BuildConfig
@ -87,8 +90,6 @@ import com.anytypeio.anytype.presentation.editor.editor.Interactor
import com.anytypeio.anytype.presentation.editor.editor.InternalDetailModificationManager
import com.anytypeio.anytype.presentation.editor.editor.Markup
import com.anytypeio.anytype.presentation.editor.editor.Orchestrator
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_models.ext.parseThemeTextColor
import com.anytypeio.anytype.presentation.editor.editor.ViewState
import com.anytypeio.anytype.presentation.editor.editor.actions.ActionItemType
import com.anytypeio.anytype.presentation.editor.editor.control.ControlPanelState
@ -111,6 +112,7 @@ import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.TXT
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
import com.anytypeio.anytype.test_utils.MockDataFactory
import com.anytypeio.anytype.test_utils.ValueClassAnswer
import com.jraska.livedata.test
@ -121,6 +123,7 @@ import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -132,6 +135,7 @@ import org.mockito.kotlin.argThat
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.given
import org.mockito.kotlin.never
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
@ -223,6 +227,9 @@ open class EditorViewModelTest {
@Mock
lateinit var downloadFile: DownloadFile
@Mock
lateinit var middlewareShareDownloader: MiddlewareShareDownloader
@Mock
lateinit var uploadBlock: UploadBlock
@ -2616,6 +2623,63 @@ open class EditorViewModelTest {
}
}
@Test
fun `should start sharing a file`() {
val root = MockDataFactory.randomUuid()
val file = MockBlockFactory.makeFileBlock()
val title = MockBlockFactory.makeTitleBlock()
val page = listOf(
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(),
children = listOf(title.id, file.id)
),
title,
file
)
val flow: Flow<List<Event.Command>> = flow {
delay(100)
emit(
listOf(
Event.Command.ShowObject(
root = root,
blocks = page,
context = root
)
)
)
}
stubObserveEvents(flow)
stubOpenPage()
givenViewModel(builder)
givenSharedFile()
vm.onStart(root)
coroutineTestRule.advanceTime(100)
// TESTING
vm.startSharingFile(id = file.id)
runTest {
verify(middlewareShareDownloader, times(1)).execute(
params = eq(
MiddlewareShareDownloader.Params(
name = file.content<Block.Content.File>().name.orEmpty(),
hash = file.content<Block.Content.File>().hash.orEmpty(),
)
)
)
}
}
@Test
fun `should start downloading file`() {
@ -3817,6 +3881,12 @@ open class EditorViewModelTest {
}
}
private fun givenSharedFile() {
middlewareShareDownloader.stub {
onBlocking { execute(any()) } doAnswer ValueClassAnswer(Uri.EMPTY)
}
}
private fun stubUpdateTextColor(root: String) {
updateTextColor.stub {
onBlocking { invoke(any()) } doReturn Either.Right(
@ -3878,7 +3948,9 @@ open class EditorViewModelTest {
vm = EditorViewModel(
openPage = openPage,
closePage = closePage,
createDocument = createDocument,
createObject = createObject,
createNewDocument = createNewDocument,
interceptEvents = interceptEvents,
interceptThreadStatus = interceptThreadStatus,
updateLinkMarks = updateLinkMark,
@ -3890,16 +3962,13 @@ open class EditorViewModelTest {
toggleStateHolder = ToggleStateHolder.Default(),
coverImageHashProvider = coverImageHashProvider
),
createDocument = createDocument,
createNewDocument = createNewDocument,
analytics = analytics,
getDefaultEditorType = getDefaultEditorType,
orchestrator = Orchestrator(
createBlock = createBlock,
replaceBlock = replaceBlock,
updateTextColor = updateTextColor,
duplicateBlock = duplicateBlock,
downloadFile = downloadFile,
middlewareShareDownloader = middlewareShareDownloader,
undo = undo,
redo = redo,
updateText = updateText,
@ -3935,22 +4004,24 @@ open class EditorViewModelTest {
createTable = createTable,
fillTableRow = fillTableRow
),
analytics = analytics,
dispatcher = Dispatcher.Default(),
delegator = delegator,
detailModificationManager = InternalDetailModificationManager(storage.details),
updateDetail = updateDetail,
getCompatibleObjectTypes = getCompatibleObjectTypes,
objectTypesProvider = objectTypesProvider,
searchObjects = searchObjects,
getDefaultEditorType = getDefaultEditorType,
findObjectSetForType = findObjectSetForType,
createObjectSet = createObjectSet,
copyFileToCache = copyFileToCacheDirectory,
downloadUnsplashImage = downloadUnsplashImage,
setDocCoverImage = setDocCoverImage,
setDocImageIcon = setDocImageIcon,
delegator = delegator,
templateDelegate = editorTemplateDelegate,
createNewObject = createNewObject,
simpleTableDelegate = simpleTableDelegate
simpleTableDelegate = simpleTableDelegate,
createNewObject = createNewObject
)
}

View file

@ -83,6 +83,7 @@ import com.anytypeio.anytype.presentation.editor.template.EditorTemplateDelegate
import com.anytypeio.anytype.presentation.editor.toggle.ToggleStateHolder
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
import com.anytypeio.anytype.test_utils.MockDataFactory
import com.anytypeio.anytype.test_utils.ValueClassAnswer
import kotlinx.coroutines.flow.Flow
@ -164,6 +165,9 @@ open class EditorPresentationTestSetup {
@Mock
lateinit var downloadFile: DownloadFile
@Mock
lateinit var middlewareShareDownloader: MiddlewareShareDownloader
@Mock
lateinit var uploadBlock: UploadBlock
@ -298,6 +302,7 @@ open class EditorPresentationTestSetup {
updateTextColor = updateTextColor,
duplicateBlock = duplicateBlock,
downloadFile = downloadFile,
middlewareShareDownloader = middlewareShareDownloader,
undo = undo,
redo = redo,
updateText = updateText,
@ -337,11 +342,13 @@ open class EditorPresentationTestSetup {
return EditorViewModel(
openPage = openPage,
closePage = closePage,
createDocument = createDocument,
createObject = createObject,
createNewDocument = createNewDocument,
interceptEvents = interceptEvents,
interceptThreadStatus = interceptThreadStatus,
updateLinkMarks = updateLinkMark,
removeLinkMark = removeLinkMark,
createObject = createObject,
reducer = DocumentExternalEventReducer(),
urlBuilder = urlBuilder,
renderer = DefaultBlockViewRenderer(
@ -349,11 +356,10 @@ open class EditorPresentationTestSetup {
toggleStateHolder = ToggleStateHolder.Default(),
coverImageHashProvider = coverImageHashProvider
),
createDocument = createDocument,
createNewDocument = createNewDocument,
analytics = analytics,
orchestrator = orchestrator,
analytics = analytics,
dispatcher = Dispatcher.Default(),
delegator = delegator,
detailModificationManager = InternalDetailModificationManager(storage.details),
updateDetail = updateDetail,
getCompatibleObjectTypes = getCompatibleObjectTypes,
@ -363,13 +369,12 @@ open class EditorPresentationTestSetup {
findObjectSetForType = findObjectSetForType,
createObjectSet = createObjectSet,
copyFileToCache = copyFileToCacheDirectory,
delegator = delegator,
downloadUnsplashImage = downloadUnsplashImage,
setDocCoverImage = setDocCoverImage,
setDocImageIcon = setDocImageIcon,
downloadUnsplashImage = downloadUnsplashImage,
templateDelegate = editorTemplateDelegate,
createNewObject = createNewObject,
simpleTableDelegate = simpleTableDelegate
simpleTableDelegate = simpleTableDelegate,
createNewObject = createNewObject
)
}