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:
parent
81ae497c8f
commit
b20fed58db
9 changed files with 180 additions and 49 deletions
|
@ -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
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.anytypeio.anytype.core_utils.common
|
||||
|
||||
data class DefaultFileInfo(
|
||||
val name: String,
|
||||
val uri: String,
|
||||
val size: Int
|
||||
)
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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("*/*")
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue