1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3098 Chats | Enhancement | Basics for uploading file attachments (#1950)

This commit is contained in:
Evgenii Kozlov 2024-12-21 17:04:49 +01:00 committed by GitHub
parent 81ae497c8f
commit b20fed58db
Signed by: github
GPG key ID: B5690EEEBB952194
9 changed files with 180 additions and 49 deletions

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.di.feature.discussions
import android.content.Context
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
@ -21,11 +22,14 @@ import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewMode
import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModelFactory
import com.anytypeio.anytype.middleware.EventProxy
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.DefaultCopyFileToCacheDirectory
import com.anytypeio.anytype.ui.home.HomeScreenFragment
import dagger.Binds
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
@Component(
dependencies = [DiscussionComponentDependencies::class],
@ -68,6 +72,14 @@ interface SpaceLevelChatComponent {
@Module
object DiscussionModule {
@JvmStatic
@Provides
@PerScreen
fun provideCopyFileToCache(
context: Context
): CopyFileToCacheDirectory = DefaultCopyFileToCacheDirectory(context)
@Module
interface Declarations {
@PerScreen
@ -93,4 +105,5 @@ interface DiscussionComponentDependencies : ComponentDependencies {
fun members(): ActiveSpaceMemberSubscriptionContainer
fun spaceViewSubscriptionContainer(): SpaceViewSubscriptionContainer
fun storeOfObjectTypes(): StoreOfObjectTypes
fun context(): Context
}

View file

@ -0,0 +1,7 @@
package com.anytypeio.anytype.core_utils.common
data class DefaultFileInfo(
val name: String,
val uri: String,
val size: Int
)

View file

@ -97,8 +97,7 @@ class CreateAccountTest {
name = name,
avatarPath = path,
icon = icon,
networkMode = NetworkMode.DEFAULT,
preferYamuxTransport = false
networkMode = NetworkMode.DEFAULT
)
onBlocking { createAccount(command) } doReturn setup
}
@ -114,8 +113,7 @@ class CreateAccountTest {
name = name,
avatarPath = path,
icon = icon,
networkMode = NetworkMode.DEFAULT,
preferYamuxTransport = false
networkMode = NetworkMode.DEFAULT
)
verify(repo, times(1)).getNetworkMode()
verify(repo, times(1)).createAccount(command)

View file

@ -61,7 +61,9 @@ sealed interface DiscussionView {
val uri: String
): ChatBoxAttachment()
data class File(
val uri: String
val uri: String,
val name: String,
val size: Int
): ChatBoxAttachment()
data class Link(
val target: Id,

View file

@ -9,6 +9,7 @@ import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.chats.Chat
import com.anytypeio.anytype.core_models.primitives.Space
import com.anytypeio.anytype.core_ui.text.splitByMarks
import com.anytypeio.anytype.core_utils.common.DefaultFileInfo
import com.anytypeio.anytype.core_utils.ext.withLatestFrom
import com.anytypeio.anytype.domain.auth.interactor.GetAccount
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
@ -35,6 +36,7 @@ import com.anytypeio.anytype.presentation.home.navigation
import com.anytypeio.anytype.presentation.mapper.objectIcon
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.search.GlobalSearchItemView
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import java.sql.Types
import javax.inject.Inject
import kotlinx.coroutines.delay
@ -44,6 +46,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combineLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
class DiscussionViewModel @Inject constructor(
@ -61,7 +64,8 @@ class DiscussionViewModel @Inject constructor(
private val spaceViews: SpaceViewSubscriptionContainer,
private val dispatchers: AppCoroutineDispatchers,
private val uploadFile: UploadFile,
private val storeOfObjectTypes: StoreOfObjectTypes
private val storeOfObjectTypes: StoreOfObjectTypes,
private val copyFileToCacheDirectory: CopyFileToCacheDirectory
) : BaseViewModel() {
val name = MutableStateFlow<String?>(null)
@ -265,18 +269,26 @@ class DiscussionViewModel @Inject constructor(
}
}
is DiscussionView.Message.ChatBoxAttachment.File -> {
uploadFile.async(
UploadFile.Params(
space = vmParams.space,
path = attachment.uri
)
).onSuccess { file ->
add(
Chat.Message.Attachment(
target = file.id,
type = Chat.Message.Attachment.Type.Image
val path = withContext(dispatchers.io) {
copyFileToCacheDirectory.copy(attachment.uri)
}
if (path != null) {
uploadFile.async(
UploadFile.Params(
space = vmParams.space,
path = path
)
)
).onSuccess { file ->
// TODO delete file.
add(
Chat.Message.Attachment(
target = file.id,
type = Chat.Message.Attachment.Type.File
)
)
}.onFailure {
Timber.e(it, "Error while uploading file as attachment")
}
}
}
}
@ -469,11 +481,13 @@ class DiscussionViewModel @Inject constructor(
}
}
fun onChatBoxFilePicked(uris: List<String>) {
Timber.d("onChatBoxFilePicked: $uris")
chatBoxAttachments.value = chatBoxAttachments.value + uris.map {
fun onChatBoxFilePicked(infos: List<DefaultFileInfo>) {
Timber.d("onChatBoxFilePicked: $infos")
chatBoxAttachments.value = chatBoxAttachments.value + infos.map { info ->
DiscussionView.Message.ChatBoxAttachment.File(
uri = it
uri = info.uri,
name = info.name,
size = info.size
)
}
}

View file

@ -18,6 +18,7 @@ import com.anytypeio.anytype.domain.`object`.OpenObject
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import javax.inject.Inject
class DiscussionViewModelFactory @Inject constructor(
@ -35,7 +36,8 @@ class DiscussionViewModelFactory @Inject constructor(
private val spaceViews: SpaceViewSubscriptionContainer,
private val dispatchers: AppCoroutineDispatchers,
private val uploadFile: UploadFile,
private val storeOfObjectTypes: StoreOfObjectTypes
private val storeOfObjectTypes: StoreOfObjectTypes,
private val copyFileToCacheDirectory: CopyFileToCacheDirectory
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = DiscussionViewModel(
@ -53,6 +55,7 @@ class DiscussionViewModelFactory @Inject constructor(
spaceViews = spaceViews,
dispatchers = dispatchers,
uploadFile = uploadFile,
storeOfObjectTypes = storeOfObjectTypes
storeOfObjectTypes = storeOfObjectTypes,
copyFileToCacheDirectory = copyFileToCacheDirectory
) as T
}

View file

@ -1,7 +1,9 @@
package com.anytypeio.anytype.feature_discussions.ui
import android.content.ContentResolver
import android.content.res.Configuration
import android.net.Uri
import android.provider.OpenableColumns
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
@ -122,6 +124,7 @@ import com.anytypeio.anytype.core_ui.views.Relations2
import com.anytypeio.anytype.core_ui.views.Relations3
import com.anytypeio.anytype.core_ui.views.fontIBM
import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon
import com.anytypeio.anytype.core_utils.common.DefaultFileInfo
import com.anytypeio.anytype.core_utils.const.DateConst.TIME_H24
import com.anytypeio.anytype.core_utils.ext.formatTimeInMillis
import com.anytypeio.anytype.core_utils.ext.parseImagePath
@ -209,7 +212,28 @@ fun DiscussionScreenWrapper(
vm.onChatBoxMediaPicked(uris.map { it.parseImagePath(context = context) })
},
onChatBoxFilePicked = { uris ->
// TODO parse path and path it vm.
val infos = uris.mapNotNull { uri ->
val cursor = context.contentResolver.query(
uri,
null,
null,
null,
null
)
if (cursor != null) {
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
DefaultFileInfo(
uri = uri.toString(),
name = cursor.getString(nameIndex),
size = cursor.getLong(sizeIndex).toInt()
)
} else {
null
}
}
vm.onChatBoxFilePicked(infos)
}
)
LaunchedEffect(Unit) {
@ -548,12 +572,8 @@ private fun ChatBox(
painter = painterResource(R.drawable.ic_clear_chatbox_attachment),
contentDescription = "Clear attachment icon",
modifier = Modifier
.align(
Alignment.TopEnd
)
.padding(
top = 6.dp
)
.align(Alignment.TopEnd)
.padding(top = 6.dp)
.noRippleClickable {
onClearAttachmentClicked(attachment)
}
@ -563,7 +583,37 @@ private fun ChatBox(
}
is DiscussionView.Message.ChatBoxAttachment.File -> {
item {
Text(text = attachment.uri)
Box {
AttachedObject(
modifier = Modifier
.padding(
top = 12.dp,
end = 4.dp
)
.width(216.dp),
title = attachment.name,
type = stringResource(R.string.file),
icon = ObjectIcon.File(
mime = null,
fileName = null
),
onAttachmentClicked = {
// TODO
}
)
Image(
painter = painterResource(id = R.drawable.ic_clear_chatbox_attachment),
contentDescription = "Close icon",
modifier = Modifier
.align(
Alignment.TopEnd
)
.padding(top = 6.dp)
.noRippleClickable {
onClearAttachmentClicked(attachment)
}
)
}
}
}
}
@ -726,24 +776,24 @@ private fun ChatBox(
)
}
)
// Divider(
// paddingStart = 0.dp,
// paddingEnd = 0.dp
// )
// DropdownMenuItem(
// text = {
// Text(
// text = stringResource(R.string.chat_attachment_file),
// color = colorResource(id = R.color.text_primary)
// )
// },
// onClick = {
// showDropdownMenu = false
// uploadFileLauncher.launch(
// arrayOf("*/*")
// )
// }
// )
Divider(
paddingStart = 0.dp,
paddingEnd = 0.dp
)
DropdownMenuItem(
text = {
Text(
text = stringResource(R.string.chat_attachment_file),
color = colorResource(id = R.color.text_primary)
)
},
onClick = {
showDropdownMenu = false
uploadFileLauncher.launch(
arrayOf("*/*")
)
}
)
}
}
}

View file

@ -1847,5 +1847,6 @@ Please provide specific details of your needs here.</string>
<string name="day_of_week_friday">Friday</string>
<string name="day_of_week_saturday">Saturday</string>
<string name="day_of_week_sunday">Sunday</string>
<string name="file">File</string>
</resources>

View file

@ -29,6 +29,8 @@ interface CopyFileToCacheDirectory {
*/
fun execute(uri: Uri, scope: CoroutineScope, listener: CopyFileToCacheStatus)
suspend fun copy(uri: String): String?
/**
* Cancels the ongoing file copying operation.
*/
@ -83,6 +85,10 @@ class DefaultCopyFileToCacheDirectory(context: Context) : CopyFileToCacheDirecto
)
}
override suspend fun copy(uri: String): String? {
return copyFileToCacheDir(uri)
}
override fun cancel() {
job?.cancel()
mContext?.get()?.deleteTemporaryFolder()
@ -145,6 +151,39 @@ class DefaultCopyFileToCacheDirectory(context: Context) : CopyFileToCacheDirecto
return null
}
private fun copyFileToCacheDir(
uri: String
): String? {
var newFile: File? = null
mContext?.get()?.let { context: Context ->
val cacheDir = context.getExternalFilesDirTemp()
if (cacheDir != null && !cacheDir.exists()) {
cacheDir.mkdirs()
}
try {
val inputStream = context.contentResolver.openInputStream(Uri.parse(uri))
inputStream?.use { input ->
newFile = File(cacheDir?.path + "/" + getFileName(context, Uri.parse(uri)));
Timber.d("Start copy file to cache : ${newFile.path}")
FileOutputStream(newFile).use { output ->
val buffer = ByteArray(1024)
var read: Int = input.read(buffer)
while (read != -1) {
output.write(buffer, 0, read)
read = input.read(buffer)
}
}
return newFile.path
}
} catch (e: Exception) {
val deleteResult = newFile?.deleteRecursively()
Timber.d("Get exception while copying file, deleteRecursively success: $deleteResult")
Timber.e(e, "Error while coping file")
}
}
return null
}
private fun getFileName(context: Context, uri: Uri): String? {
var result: String? = null
if (uri.scheme == SCHEME_CONTENT) {
@ -199,6 +238,10 @@ class NetworkModeCopyFileToCacheDirectory(context: Context) : CopyFileToCacheDir
)
}
override suspend fun copy(uri: String): String? {
throw UnsupportedOperationException()
}
override fun cancel() {
job?.cancel()
}