Merge pull request #1099 from vector-im/feature/fix_share_image
Share images from clear and encrypted rooms.
This commit is contained in:
commit
b929a2f185
@ -5,6 +5,7 @@ Features ✨:
|
|||||||
-
|
-
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
|
- Share image and other media from e2e rooms (#677)
|
||||||
- Add support for `/plain` command (#12)
|
- Add support for `/plain` command (#12)
|
||||||
- Detect spaces in password if user fail to login (#1038)
|
- Detect spaces in password if user fail to login (#1038)
|
||||||
- FTUE: do not display a different color when encrypting message when not in developer mode.
|
- FTUE: do not display a different color when encrypting message when not in developer mode.
|
||||||
@ -12,6 +13,8 @@ Improvements 🙌:
|
|||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Fix crash on attachment preview screen (#1088)
|
- Fix crash on attachment preview screen (#1088)
|
||||||
|
- "Share" option is not appearing in encrypted rooms for images (#1031)
|
||||||
|
- Set "image/jpeg" as MIME type of images instead of "image/jpg" (#1075)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
-
|
-
|
||||||
|
@ -126,7 +126,7 @@ dependencies {
|
|||||||
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
||||||
|
|
||||||
// Work
|
// Work
|
||||||
implementation "androidx.work:work-runtime-ktx:2.3.0"
|
implementation "androidx.work:work-runtime-ktx:2.3.3"
|
||||||
|
|
||||||
// FP
|
// FP
|
||||||
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
||||||
|
@ -31,7 +31,7 @@ data class ContentAttachmentData(
|
|||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val queryUri: String,
|
val queryUri: String,
|
||||||
val path: String,
|
val path: String,
|
||||||
val mimeType: String?,
|
private val mimeType: String?,
|
||||||
val type: Type
|
val type: Type
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
@ -41,4 +41,6 @@ data class ContentAttachmentData(
|
|||||||
AUDIO,
|
AUDIO,
|
||||||
VIDEO
|
VIDEO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSafeMimeType() = if (mimeType == "image/jpg") "image/jpeg" else mimeType
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,11 @@ interface FileService {
|
|||||||
/**
|
/**
|
||||||
* Download file in cache
|
* Download file in cache
|
||||||
*/
|
*/
|
||||||
FOR_INTERNAL_USE
|
FOR_INTERNAL_USE,
|
||||||
|
/**
|
||||||
|
* Download file in file provider path
|
||||||
|
*/
|
||||||
|
FOR_EXTERNAL_SHARE
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,4 +51,4 @@ data class MessageAudioContent(
|
|||||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
*/
|
*/
|
||||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
) : MessageEncryptedContent
|
) : MessageWithAttachmentContent
|
||||||
|
@ -57,7 +57,7 @@ data class MessageFileContent(
|
|||||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
*/
|
*/
|
||||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
) : MessageEncryptedContent {
|
) : MessageWithAttachmentContent {
|
||||||
|
|
||||||
fun getMimeType(): String {
|
fun getMimeType(): String {
|
||||||
// Mimetype default to plain text, should not be used
|
// Mimetype default to plain text, should not be used
|
||||||
|
@ -20,6 +20,6 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
/**
|
/**
|
||||||
* A content with image information
|
* A content with image information
|
||||||
*/
|
*/
|
||||||
interface MessageImageInfoContent : MessageEncryptedContent {
|
interface MessageImageInfoContent : MessageWithAttachmentContent {
|
||||||
val info: ImageInfo?
|
val info: ImageInfo?
|
||||||
}
|
}
|
||||||
|
@ -51,4 +51,4 @@ data class MessageVideoContent(
|
|||||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
*/
|
*/
|
||||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
) : MessageEncryptedContent
|
) : MessageWithAttachmentContent
|
||||||
|
@ -21,7 +21,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
|||||||
/**
|
/**
|
||||||
* Interface for message which can contains an encrypted file
|
* Interface for message which can contains an encrypted file
|
||||||
*/
|
*/
|
||||||
interface MessageEncryptedContent : MessageContent {
|
interface MessageWithAttachmentContent : MessageContent {
|
||||||
/**
|
/**
|
||||||
* Required if the file is unencrypted. The URL (typically MXC URI) to the image.
|
* Required if the file is unencrypted. The URL (typically MXC URI) to the image.
|
||||||
*/
|
*/
|
||||||
@ -36,4 +36,4 @@ interface MessageEncryptedContent : MessageContent {
|
|||||||
/**
|
/**
|
||||||
* Get the url of the encrypted file or of the file
|
* Get the url of the encrypted file or of the file
|
||||||
*/
|
*/
|
||||||
fun MessageEncryptedContent.getFileUrl() = encryptedFileInfo?.url ?: url
|
fun MessageWithAttachmentContent.getFileUrl() = encryptedFileInfo?.url ?: url
|
@ -25,3 +25,7 @@ annotation class SessionFilesDirectory
|
|||||||
@Qualifier
|
@Qualifier
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
annotation class SessionCacheDirectory
|
annotation class SessionCacheDirectory
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class CacheDirectory
|
||||||
|
@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
|||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.matrix.olm.OlmManager
|
import org.matrix.olm.OlmManager
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class])
|
@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class])
|
||||||
@MatrixScope
|
@MatrixScope
|
||||||
@ -52,6 +53,9 @@ internal interface MatrixComponent {
|
|||||||
|
|
||||||
fun resources(): Resources
|
fun resources(): Resources
|
||||||
|
|
||||||
|
@CacheDirectory
|
||||||
|
fun cacheDir(): File
|
||||||
|
|
||||||
fun olmManager(): OlmManager
|
fun olmManager(): OlmManager
|
||||||
|
|
||||||
fun taskExecutor(): TaskExecutor
|
fun taskExecutor(): TaskExecutor
|
||||||
|
@ -26,6 +26,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.android.asCoroutineDispatcher
|
import kotlinx.coroutines.android.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import org.matrix.olm.OlmManager
|
import org.matrix.olm.OlmManager
|
||||||
|
import java.io.File
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ -49,6 +50,13 @@ internal object MatrixModule {
|
|||||||
return context.resources
|
return context.resources
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@CacheDirectory
|
||||||
|
fun providesCacheDir(context: Context): File {
|
||||||
|
return context.cacheDir
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Provides
|
@Provides
|
||||||
@MatrixScope
|
@MatrixScope
|
||||||
|
@ -24,11 +24,11 @@ import im.vector.matrix.android.api.session.file.FileService
|
|||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||||
|
import im.vector.matrix.android.internal.di.CacheDirectory
|
||||||
import im.vector.matrix.android.internal.di.SessionCacheDirectory
|
import im.vector.matrix.android.internal.di.SessionCacheDirectory
|
||||||
import im.vector.matrix.android.internal.di.Unauthenticated
|
import im.vector.matrix.android.internal.di.Unauthenticated
|
||||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import im.vector.matrix.android.internal.util.md5
|
|
||||||
import im.vector.matrix.android.internal.util.toCancelable
|
import im.vector.matrix.android.internal.util.toCancelable
|
||||||
import im.vector.matrix.android.internal.util.writeToFile
|
import im.vector.matrix.android.internal.util.writeToFile
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@ -42,8 +42,10 @@ import java.io.IOException
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultFileService @Inject constructor(
|
internal class DefaultFileService @Inject constructor(
|
||||||
@SessionCacheDirectory
|
@CacheDirectory
|
||||||
private val cacheDirectory: File,
|
private val cacheDirectory: File,
|
||||||
|
@SessionCacheDirectory
|
||||||
|
private val sessionCacheDirectory: File,
|
||||||
private val contentUrlResolver: ContentUrlResolver,
|
private val contentUrlResolver: ContentUrlResolver,
|
||||||
@Unauthenticated
|
@Unauthenticated
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
@ -62,60 +64,50 @@ internal class DefaultFileService @Inject constructor(
|
|||||||
return GlobalScope.launch(coroutineDispatchers.main) {
|
return GlobalScope.launch(coroutineDispatchers.main) {
|
||||||
withContext(coroutineDispatchers.io) {
|
withContext(coroutineDispatchers.io) {
|
||||||
Try {
|
Try {
|
||||||
val folder = getFolder(downloadMode, id)
|
val folder = File(sessionCacheDirectory, "MF")
|
||||||
|
if (!folder.exists()) {
|
||||||
|
folder.mkdirs()
|
||||||
|
}
|
||||||
File(folder, fileName)
|
File(folder, fileName)
|
||||||
}.flatMap { destFile ->
|
}.flatMap { destFile ->
|
||||||
if (!destFile.exists() || downloadMode == FileService.DownloadMode.TO_EXPORT) {
|
if (!destFile.exists()) {
|
||||||
Try {
|
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
|
||||||
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null")
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(resolvedUrl)
|
.url(resolvedUrl)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val response = okHttpClient.newCall(request).execute()
|
val response = okHttpClient.newCall(request).execute()
|
||||||
var inputStream = response.body?.byteStream()
|
var inputStream = response.body?.byteStream()
|
||||||
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}")
|
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}")
|
||||||
if (!response.isSuccessful
|
if (!response.isSuccessful || inputStream == null) {
|
||||||
|| inputStream == null) {
|
return@flatMap Try.Failure(IOException())
|
||||||
throw IOException()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (elementToDecrypt != null) {
|
|
||||||
Timber.v("## decrypt file")
|
|
||||||
inputStream = MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
|
|
||||||
?: throw IllegalStateException("Decryption error")
|
|
||||||
}
|
|
||||||
|
|
||||||
writeToFile(inputStream, destFile)
|
|
||||||
destFile
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Try.just(destFile)
|
if (elementToDecrypt != null) {
|
||||||
|
Timber.v("## decrypt file")
|
||||||
|
inputStream = MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
|
||||||
|
?: return@flatMap Try.Failure(IllegalStateException("Decryption error"))
|
||||||
|
}
|
||||||
|
|
||||||
|
writeToFile(inputStream, destFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Try.just(copyFile(destFile, downloadMode))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.foldToCallback(callback)
|
.foldToCallback(callback)
|
||||||
}.toCancelable()
|
}.toCancelable()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFolder(downloadMode: FileService.DownloadMode, id: String): File {
|
private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File {
|
||||||
return when (downloadMode) {
|
return when (downloadMode) {
|
||||||
FileService.DownloadMode.FOR_INTERNAL_USE -> {
|
FileService.DownloadMode.TO_EXPORT ->
|
||||||
// Create dir tree (MF stands for Matrix File):
|
file.copyTo(File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), file.name), true)
|
||||||
// <cache>/<sessionId>/MF/<md5(id)>/
|
FileService.DownloadMode.FOR_EXTERNAL_SHARE ->
|
||||||
val tmpFolderSession = File(cacheDirectory, "MF")
|
file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true)
|
||||||
File(tmpFolderSession, id.md5())
|
FileService.DownloadMode.FOR_INTERNAL_USE ->
|
||||||
}
|
file
|
||||||
FileService.DownloadMode.TO_EXPORT -> {
|
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.also { folder ->
|
|
||||||
if (!folder.exists()) {
|
|
||||||
folder.mkdirs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
@ -33,7 +34,13 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
|
|||||||
val listeners = listeners.getOrPut(key) { ArrayList() }
|
val listeners = listeners.getOrPut(key) { ArrayList() }
|
||||||
listeners.add(updateListener)
|
listeners.add(updateListener)
|
||||||
val currentState = states[key] ?: ContentUploadStateTracker.State.Idle
|
val currentState = states[key] ?: ContentUploadStateTracker.State.Idle
|
||||||
mainHandler.post { updateListener.onUpdate(currentState) }
|
mainHandler.post {
|
||||||
|
try {
|
||||||
|
updateListener.onUpdate(currentState)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun untrack(key: String, updateListener: ContentUploadStateTracker.UpdateListener) {
|
override fun untrack(key: String, updateListener: ContentUploadStateTracker.UpdateListener) {
|
||||||
@ -79,7 +86,13 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
|
|||||||
private fun updateState(key: String, state: ContentUploadStateTracker.State) {
|
private fun updateState(key: String, state: ContentUploadStateTracker.State) {
|
||||||
states[key] = state
|
states[key] = state
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
listeners[key]?.forEach { it.onUpdate(state) }
|
listeners[key]?.forEach {
|
||||||
|
try {
|
||||||
|
it.onUpdate(state)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||||||
override val sessionId: String,
|
override val sessionId: String,
|
||||||
val events: List<Event>,
|
val events: List<Event>,
|
||||||
val attachment: ContentAttachmentData,
|
val attachment: ContentAttachmentData,
|
||||||
val isRoomEncrypted: Boolean,
|
val isEncrypted: Boolean,
|
||||||
val compressBeforeSending: Boolean,
|
val compressBeforeSending: Boolean,
|
||||||
override val lastFailureMessage: String? = null
|
override val lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
@ -90,9 +90,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
|
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
|
||||||
return Result.success(
|
return Result.success(
|
||||||
WorkerParamsFactory.toData(params.copy(
|
WorkerParamsFactory.toData(
|
||||||
lastFailureMessage = e.localizedMessage
|
params.copy(
|
||||||
))
|
lastFailureMessage = e.localizedMessage
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.let { originalFile ->
|
.let { originalFile ->
|
||||||
@ -136,7 +138,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val contentUploadResponse = if (params.isRoomEncrypted) {
|
val contentUploadResponse = if (params.isEncrypted) {
|
||||||
Timber.v("Encrypt thumbnail")
|
Timber.v("Encrypt thumbnail")
|
||||||
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
|
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
|
||||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||||
@ -174,18 +176,18 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||||||
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val contentUploadResponse = if (params.isRoomEncrypted) {
|
val contentUploadResponse = if (params.isEncrypted) {
|
||||||
Timber.v("Encrypt file")
|
Timber.v("Encrypt file")
|
||||||
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
|
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
|
||||||
|
|
||||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
|
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.getSafeMimeType())
|
||||||
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||||
|
|
||||||
fileUploader
|
fileUploader
|
||||||
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
|
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
|
||||||
} else {
|
} else {
|
||||||
fileUploader
|
fileUploader
|
||||||
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
|
.uploadFile(attachmentFile, attachment.name, attachment.getSafeMimeType(), progressListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSuccess(params,
|
handleSuccess(params,
|
||||||
@ -226,7 +228,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||||||
updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes)
|
updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isRoomEncrypted)
|
val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted)
|
||||||
return Result.success(WorkerParamsFactory.toData(sendParams))
|
return Result.success(WorkerParamsFactory.toData(sendParams))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,7 +261,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
msgType = MessageType.MSGTYPE_IMAGE,
|
msgType = MessageType.MSGTYPE_IMAGE,
|
||||||
body = attachment.name ?: "image",
|
body = attachment.name ?: "image",
|
||||||
info = ImageInfo(
|
info = ImageInfo(
|
||||||
mimeType = attachment.mimeType,
|
mimeType = attachment.getSafeMimeType(),
|
||||||
width = width?.toInt() ?: 0,
|
width = width?.toInt() ?: 0,
|
||||||
height = height?.toInt() ?: 0,
|
height = height?.toInt() ?: 0,
|
||||||
size = attachment.size.toInt()
|
size = attachment.size.toInt()
|
||||||
@ -293,7 +293,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
msgType = MessageType.MSGTYPE_VIDEO,
|
msgType = MessageType.MSGTYPE_VIDEO,
|
||||||
body = attachment.name ?: "video",
|
body = attachment.name ?: "video",
|
||||||
videoInfo = VideoInfo(
|
videoInfo = VideoInfo(
|
||||||
mimeType = attachment.mimeType,
|
mimeType = attachment.getSafeMimeType(),
|
||||||
width = width,
|
width = width,
|
||||||
height = height,
|
height = height,
|
||||||
size = attachment.size,
|
size = attachment.size,
|
||||||
@ -312,7 +312,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
msgType = MessageType.MSGTYPE_AUDIO,
|
msgType = MessageType.MSGTYPE_AUDIO,
|
||||||
body = attachment.name ?: "audio",
|
body = attachment.name ?: "audio",
|
||||||
audioInfo = AudioInfo(
|
audioInfo = AudioInfo(
|
||||||
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
||||||
size = attachment.size
|
size = attachment.size
|
||||||
),
|
),
|
||||||
url = attachment.path
|
url = attachment.path
|
||||||
@ -325,7 +325,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
msgType = MessageType.MSGTYPE_FILE,
|
msgType = MessageType.MSGTYPE_FILE,
|
||||||
body = attachment.name ?: "file",
|
body = attachment.name ?: "file",
|
||||||
info = FileInfo(
|
info = FileInfo(
|
||||||
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() }
|
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }
|
||||||
?: "application/octet-stream",
|
?: "application/octet-stream",
|
||||||
size = attachment.size
|
size = attachment.size
|
||||||
),
|
),
|
||||||
|
@ -296,7 +296,7 @@ dependencies {
|
|||||||
implementation 'com.airbnb.android:mvrx:1.3.0'
|
implementation 'com.airbnb.android:mvrx:1.3.0'
|
||||||
|
|
||||||
// Work
|
// Work
|
||||||
implementation "androidx.work:work-runtime-ktx:2.3.0-beta02"
|
implementation "androidx.work:work-runtime-ktx:2.3.3"
|
||||||
|
|
||||||
// Paging
|
// Paging
|
||||||
implementation "androidx.paging:paging-runtime-ktx:2.1.1"
|
implementation "androidx.paging:paging-runtime-ktx:2.1.1"
|
||||||
|
@ -27,6 +27,7 @@ import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_CAMERA
|
|||||||
import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_DEVICE
|
import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_DEVICE
|
||||||
import com.kbeanie.multipicker.core.ImagePickerImpl
|
import com.kbeanie.multipicker.core.ImagePickerImpl
|
||||||
import com.kbeanie.multipicker.core.PickerManager
|
import com.kbeanie.multipicker.core.PickerManager
|
||||||
|
import com.kbeanie.multipicker.utils.IntentUtils
|
||||||
import im.vector.matrix.android.BuildConfig
|
import im.vector.matrix.android.BuildConfig
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import im.vector.riotx.core.platform.Restorable
|
import im.vector.riotx.core.platform.Restorable
|
||||||
@ -176,13 +177,13 @@ class AttachmentsHelper private constructor(private val context: Context,
|
|||||||
fun handleShareIntent(intent: Intent): Boolean {
|
fun handleShareIntent(intent: Intent): Boolean {
|
||||||
val type = intent.resolveType(context) ?: return false
|
val type = intent.resolveType(context) ?: return false
|
||||||
if (type.startsWith("image")) {
|
if (type.startsWith("image")) {
|
||||||
imagePicker.submit(intent)
|
imagePicker.submit(IntentUtils.getPickerIntentForSharing(intent))
|
||||||
} else if (type.startsWith("video")) {
|
} else if (type.startsWith("video")) {
|
||||||
videoPicker.submit(intent)
|
videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent))
|
||||||
} else if (type.startsWith("audio")) {
|
} else if (type.startsWith("audio")) {
|
||||||
videoPicker.submit(intent)
|
videoPicker.submit(IntentUtils.getPickerIntentForSharing(intent))
|
||||||
} else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
|
} else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
|
||||||
filePicker.submit(intent)
|
filePicker.submit(IntentUtils.getPickerIntentForSharing(intent))
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,6 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
|||||||
*/
|
*/
|
||||||
fun ContentAttachmentData.isEditable(): Boolean {
|
fun ContentAttachmentData.isEditable(): Boolean {
|
||||||
return type == ContentAttachmentData.Type.IMAGE
|
return type == ContentAttachmentData.Type.IMAGE
|
||||||
&& mimeType?.startsWith("image/") == true
|
&& getSafeMimeType()?.startsWith("image/") == true
|
||||||
&& mimeType != "image/gif"
|
&& getSafeMimeType() != "image/gif"
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.core.text.buildSpannedString
|
import androidx.core.text.buildSpannedString
|
||||||
import androidx.core.util.Pair
|
import androidx.core.util.Pair
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
@ -57,17 +58,17 @@ import com.airbnb.mvrx.Success
|
|||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.args
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import com.github.piasy.biv.BigImageViewer
|
|
||||||
import com.github.piasy.biv.loader.ImageLoader
|
|
||||||
import com.google.android.material.checkbox.MaterialCheckBox
|
import com.google.android.material.checkbox.MaterialCheckBox
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.jakewharton.rxbinding3.widget.textChanges
|
import com.jakewharton.rxbinding3.widget.textChanges
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.file.FileService
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
@ -77,12 +78,14 @@ import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoC
|
|||||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
||||||
import im.vector.matrix.android.api.util.MatrixItem
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
import im.vector.matrix.android.api.util.toMatrixItem
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
|
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.dialogs.withColoredButton
|
import im.vector.riotx.core.dialogs.withColoredButton
|
||||||
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
|
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
|
||||||
@ -93,6 +96,7 @@ import im.vector.riotx.core.extensions.setTextOrHide
|
|||||||
import im.vector.riotx.core.extensions.showKeyboard
|
import im.vector.riotx.core.extensions.showKeyboard
|
||||||
import im.vector.riotx.core.files.addEntryToDownloadManager
|
import im.vector.riotx.core.files.addEntryToDownloadManager
|
||||||
import im.vector.riotx.core.glide.GlideApp
|
import im.vector.riotx.core.glide.GlideApp
|
||||||
|
import im.vector.riotx.core.intent.getMimeTypeFromUri
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import im.vector.riotx.core.resources.ColorProvider
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
|
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
|
||||||
@ -1124,6 +1128,23 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
|
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onShareActionClicked(action: EventSharedAction.Share) {
|
||||||
|
session.downloadFile(
|
||||||
|
FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
||||||
|
action.eventId,
|
||||||
|
action.messageContent.body,
|
||||||
|
action.messageContent.getFileUrl(),
|
||||||
|
action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||||
|
object : MatrixCallback<File> {
|
||||||
|
override fun onSuccess(data: File) {
|
||||||
|
if (isAdded) {
|
||||||
|
shareMedia(requireContext(), data, getMimeTypeFromUri(requireContext(), data.toUri()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleActions(action: EventSharedAction) {
|
private fun handleActions(action: EventSharedAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is EventSharedAction.OpenUserProfile -> {
|
is EventSharedAction.OpenUserProfile -> {
|
||||||
@ -1145,32 +1166,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
promptConfirmationToRedactEvent(action)
|
promptConfirmationToRedactEvent(action)
|
||||||
}
|
}
|
||||||
is EventSharedAction.Share -> {
|
is EventSharedAction.Share -> {
|
||||||
// TODO current data communication is too limited
|
onShareActionClicked(action)
|
||||||
// Need to now the media type
|
|
||||||
// TODO bad, just POC
|
|
||||||
BigImageViewer.imageLoader().loadImage(
|
|
||||||
action.hashCode(),
|
|
||||||
Uri.parse(action.imageUrl),
|
|
||||||
object : ImageLoader.Callback {
|
|
||||||
override fun onFinish() {}
|
|
||||||
|
|
||||||
override fun onSuccess(image: File?) {
|
|
||||||
if (image != null) {
|
|
||||||
shareMedia(requireContext(), image, "image/*")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFail(error: Exception?) {}
|
|
||||||
|
|
||||||
override fun onCacheHit(imageType: Int, image: File?) {}
|
|
||||||
|
|
||||||
override fun onCacheMiss(imageType: Int, image: File?) {}
|
|
||||||
|
|
||||||
override fun onProgress(progress: Int) {}
|
|
||||||
|
|
||||||
override fun onStart() {}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
is EventSharedAction.ViewEditHistory -> {
|
is EventSharedAction.ViewEditHistory -> {
|
||||||
onEditedDecorationClicked(action.messageInformationData)
|
onEditedDecorationClicked(action.messageInformationData)
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.action
|
|||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.platform.VectorSharedAction
|
import im.vector.riotx.core.platform.VectorSharedAction
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
@ -46,7 +47,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int,
|
|||||||
data class Reply(val eventId: String) :
|
data class Reply(val eventId: String) :
|
||||||
EventSharedAction(R.string.reply, R.drawable.ic_reply)
|
EventSharedAction(R.string.reply, R.drawable.ic_reply)
|
||||||
|
|
||||||
data class Share(val imageUrl: String) :
|
data class Share(val eventId: String, val messageContent: MessageWithAttachmentContent) :
|
||||||
EventSharedAction(R.string.share, R.drawable.ic_share)
|
EventSharedAction(R.string.share, R.drawable.ic_share)
|
||||||
|
|
||||||
data class Resend(val eventId: String) :
|
data class Resend(val eventId: String) :
|
||||||
|
@ -29,8 +29,8 @@ import im.vector.matrix.android.api.session.events.model.EventType
|
|||||||
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageFormat
|
import im.vector.matrix.android.api.session.room.model.message.MessageFormat
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||||
@ -260,13 +260,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||||||
add(EventSharedAction.ViewEditHistory(informationData))
|
add(EventSharedAction.ViewEditHistory(informationData))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canShare(msgType)) {
|
if (canShare(msgType) && messageContent is MessageWithAttachmentContent) {
|
||||||
if (messageContent is MessageImageContent) {
|
add(EventSharedAction.Share(timelineEvent.eventId, messageContent))
|
||||||
session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
|
|
||||||
add(EventSharedAction.Share(url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timelineEvent.root.sendState == SendState.SENT) {
|
if (timelineEvent.root.sendState == SendState.SENT) {
|
||||||
@ -374,8 +369,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||||||
return when (msgType) {
|
return when (msgType) {
|
||||||
MessageType.MSGTYPE_IMAGE,
|
MessageType.MSGTYPE_IMAGE,
|
||||||
MessageType.MSGTYPE_AUDIO,
|
MessageType.MSGTYPE_AUDIO,
|
||||||
MessageType.MSGTYPE_VIDEO -> true
|
MessageType.MSGTYPE_VIDEO,
|
||||||
else -> false
|
MessageType.MSGTYPE_FILE -> true
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,6 +127,15 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
|||||||
GlideApp
|
GlideApp
|
||||||
.with(imageView)
|
.with(imageView)
|
||||||
.load(resolvedUrl)
|
.load(resolvedUrl)
|
||||||
|
.apply {
|
||||||
|
if (mode == Mode.THUMBNAIL) {
|
||||||
|
error(
|
||||||
|
GlideApp
|
||||||
|
.with(imageView)
|
||||||
|
.load(contentUrlResolver.resolveFullSize(data.url))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ import androidx.appcompat.widget.SearchView
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import com.kbeanie.multipicker.utils.IntentUtils
|
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
@ -78,7 +77,7 @@ class IncomingShareFragment @Inject constructor(
|
|||||||
val intent = vectorBaseActivity.intent
|
val intent = vectorBaseActivity.intent
|
||||||
val isShareManaged = when (intent?.action) {
|
val isShareManaged = when (intent?.action) {
|
||||||
Intent.ACTION_SEND -> {
|
Intent.ACTION_SEND -> {
|
||||||
var isShareManaged = attachmentsHelper.handleShareIntent(IntentUtils.getPickerIntentForSharing(intent))
|
var isShareManaged = attachmentsHelper.handleShareIntent(intent)
|
||||||
if (!isShareManaged) {
|
if (!isShareManaged) {
|
||||||
isShareManaged = handleTextShare(intent)
|
isShareManaged = handleTextShare(intent)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user