Merge pull request #1010 from vector-im/feature/attachment_process
Attachment process
This commit is contained in:
commit
8d1b2b35fd
@ -3,7 +3,10 @@ Changes in RiotX 0.17.0 (2020-XX-XX)
|
|||||||
|
|
||||||
Features ✨:
|
Features ✨:
|
||||||
- Secured Shared Storage Support (#984, #936)
|
- Secured Shared Storage Support (#984, #936)
|
||||||
- Polls and Bot Buttons (MSC 2192 matrix-org/matrix-doc#2192)
|
- It's now possible to select several rooms (with a possible mix of clear/encrypted rooms) when sharing elements to RiotX (#1010)
|
||||||
|
- Media preview: media are previewed before being sent to a room (#1010)
|
||||||
|
- Image edition: it's now possible to edit image before sending: crop, rotate, and delete actions are supported (#1010)
|
||||||
|
- Sending image: image are sent to rooms with a reduced size. It's still possible to send original image file (#1010)
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
-
|
-
|
||||||
|
@ -34,6 +34,9 @@ allprojects {
|
|||||||
includeGroupByRegex "com\\.github\\.jaiselrahman"
|
includeGroupByRegex "com\\.github\\.jaiselrahman"
|
||||||
// And monarchy
|
// And monarchy
|
||||||
includeGroupByRegex "com\\.github\\.Zhuinden"
|
includeGroupByRegex "com\\.github\\.Zhuinden"
|
||||||
|
// And ucrop
|
||||||
|
includeGroupByRegex "com\\.github\\.yalantis"
|
||||||
|
// JsonViewer
|
||||||
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
|
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,7 @@ dependencies {
|
|||||||
|
|
||||||
// Image
|
// Image
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.1.0'
|
implementation 'androidx.exifinterface:exifinterface:1.1.0'
|
||||||
|
implementation 'id.zelory:compressor:3.0.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
|
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
|
||||||
|
@ -29,6 +29,7 @@ data class ContentAttachmentData(
|
|||||||
val width: Long? = 0,
|
val width: Long? = 0,
|
||||||
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
|
val queryUri: String,
|
||||||
val path: String,
|
val path: String,
|
||||||
val mimeType: String?,
|
val mimeType: String?,
|
||||||
val type: Type
|
val type: Type
|
||||||
|
@ -51,16 +51,26 @@ interface SendService {
|
|||||||
/**
|
/**
|
||||||
* Method to send a media asynchronously.
|
* Method to send a media asynchronously.
|
||||||
* @param attachment the media to send
|
* @param attachment the media to send
|
||||||
|
* @param compressBeforeSending set to true to compress images before sending them
|
||||||
|
* @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present.
|
||||||
|
* It can be useful to send media to multiple room. It's safe to include the current roomId in this set
|
||||||
* @return a [Cancelable]
|
* @return a [Cancelable]
|
||||||
*/
|
*/
|
||||||
fun sendMedia(attachment: ContentAttachmentData): Cancelable
|
fun sendMedia(attachment: ContentAttachmentData,
|
||||||
|
compressBeforeSending: Boolean,
|
||||||
|
roomIds: Set<String>): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to send a list of media asynchronously.
|
* Method to send a list of media asynchronously.
|
||||||
* @param attachments the list of media to send
|
* @param attachments the list of media to send
|
||||||
|
* @param compressBeforeSending set to true to compress images before sending them
|
||||||
|
* @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present.
|
||||||
|
* It can be useful to send media to multiple room. It's safe to include the current roomId in this set
|
||||||
* @return a [Cancelable]
|
* @return a [Cancelable]
|
||||||
*/
|
*/
|
||||||
fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable
|
fun sendMedias(attachments: List<ContentAttachmentData>,
|
||||||
|
compressBeforeSending: Boolean,
|
||||||
|
roomIds: Set<String>): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a poll to the room.
|
* Send a poll to the room.
|
||||||
|
@ -17,7 +17,11 @@
|
|||||||
package im.vector.matrix.android.internal.di
|
package im.vector.matrix.android.internal.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.work.*
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.ListenableWorker
|
||||||
|
import androidx.work.NetworkType
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
import androidx.work.WorkManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class WorkManagerProvider @Inject constructor(
|
internal class WorkManagerProvider @Inject constructor(
|
||||||
@ -54,5 +58,7 @@ internal class WorkManagerProvider @Inject constructor(
|
|||||||
val workConstraints = Constraints.Builder()
|
val workConstraints = Constraints.Builder()
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
const val BACKOFF_DELAY = 10_000L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ import im.vector.matrix.android.internal.session.pushers.PushersModule
|
|||||||
import im.vector.matrix.android.internal.session.room.RoomModule
|
import im.vector.matrix.android.internal.session.room.RoomModule
|
||||||
import im.vector.matrix.android.internal.session.room.relation.SendRelationWorker
|
import im.vector.matrix.android.internal.session.room.relation.SendRelationWorker
|
||||||
import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker
|
import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker
|
||||||
|
import im.vector.matrix.android.internal.session.room.send.MultipleEventSendingDispatcherWorker
|
||||||
import im.vector.matrix.android.internal.session.room.send.RedactEventWorker
|
import im.vector.matrix.android.internal.session.room.send.RedactEventWorker
|
||||||
import im.vector.matrix.android.internal.session.room.send.SendEventWorker
|
import im.vector.matrix.android.internal.session.room.send.SendEventWorker
|
||||||
import im.vector.matrix.android.internal.session.signout.SignOutModule
|
import im.vector.matrix.android.internal.session.signout.SignOutModule
|
||||||
@ -85,23 +86,25 @@ internal interface SessionComponent {
|
|||||||
|
|
||||||
fun taskExecutor(): TaskExecutor
|
fun taskExecutor(): TaskExecutor
|
||||||
|
|
||||||
fun inject(sendEventWorker: SendEventWorker)
|
fun inject(worker: SendEventWorker)
|
||||||
|
|
||||||
fun inject(sendEventWorker: SendRelationWorker)
|
fun inject(worker: SendRelationWorker)
|
||||||
|
|
||||||
fun inject(encryptEventWorker: EncryptEventWorker)
|
fun inject(worker: EncryptEventWorker)
|
||||||
|
|
||||||
fun inject(redactEventWorker: RedactEventWorker)
|
fun inject(worker: MultipleEventSendingDispatcherWorker)
|
||||||
|
|
||||||
fun inject(getGroupDataWorker: GetGroupDataWorker)
|
fun inject(worker: RedactEventWorker)
|
||||||
|
|
||||||
fun inject(uploadContentWorker: UploadContentWorker)
|
fun inject(worker: GetGroupDataWorker)
|
||||||
|
|
||||||
fun inject(syncWorker: SyncWorker)
|
fun inject(worker: UploadContentWorker)
|
||||||
|
|
||||||
fun inject(addHttpPusherWorker: AddHttpPusherWorker)
|
fun inject(worker: SyncWorker)
|
||||||
|
|
||||||
fun inject(sendVerificationMessageWorker: SendVerificationMessageWorker)
|
fun inject(worker: AddHttpPusherWorker)
|
||||||
|
|
||||||
|
fun inject(worker: SendVerificationMessageWorker)
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
@ -17,18 +17,25 @@
|
|||||||
package im.vector.matrix.android.internal.session.content
|
package im.vector.matrix.android.internal.session.content
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import id.zelory.compressor.Compressor
|
||||||
|
import id.zelory.compressor.constraint.default
|
||||||
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.events.model.toContent
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
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.*
|
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.MessageFileContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
||||||
import im.vector.matrix.android.internal.session.room.send.SendEventWorker
|
import im.vector.matrix.android.internal.session.room.send.MultipleEventSendingDispatcherWorker
|
||||||
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
@ -38,15 +45,21 @@ import java.io.File
|
|||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class UploadContentWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
private data class NewImageAttributes(
|
||||||
|
val newWidth: Int?,
|
||||||
|
val newHeight: Int?,
|
||||||
|
val newFileSize: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
internal class UploadContentWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class Params(
|
internal data class Params(
|
||||||
override val sessionId: String,
|
override val sessionId: String,
|
||||||
val roomId: String,
|
val events: List<Event>,
|
||||||
val event: Event,
|
|
||||||
val attachment: ContentAttachmentData,
|
val attachment: ContentAttachmentData,
|
||||||
val isRoomEncrypted: Boolean,
|
val isRoomEncrypted: Boolean,
|
||||||
|
val compressBeforeSending: Boolean,
|
||||||
override val lastFailureMessage: String? = null
|
override val lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@ -67,20 +80,50 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
|
|
||||||
val eventId = params.event.eventId ?: return Result.success()
|
|
||||||
val attachment = params.attachment
|
val attachment = params.attachment
|
||||||
|
|
||||||
|
var newImageAttributes: NewImageAttributes? = null
|
||||||
|
|
||||||
val attachmentFile = try {
|
val attachmentFile = try {
|
||||||
File(attachment.path)
|
File(attachment.path)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
contentUploadStateTracker.setFailure(params.event.eventId, e)
|
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
|
||||||
return Result.success(
|
return Result.success(
|
||||||
WorkerParamsFactory.toData(params.copy(
|
WorkerParamsFactory.toData(params.copy(
|
||||||
lastFailureMessage = e.localizedMessage
|
lastFailureMessage = e.localizedMessage
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.let { originalFile ->
|
||||||
|
if (attachment.type == ContentAttachmentData.Type.IMAGE) {
|
||||||
|
if (params.compressBeforeSending) {
|
||||||
|
Compressor.compress(context, originalFile) {
|
||||||
|
default(
|
||||||
|
width = MAX_IMAGE_SIZE,
|
||||||
|
height = MAX_IMAGE_SIZE
|
||||||
|
)
|
||||||
|
}.also { compressedFile ->
|
||||||
|
// Update the params
|
||||||
|
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||||
|
BitmapFactory.decodeFile(compressedFile.absolutePath, options)
|
||||||
|
val fileSize = compressedFile.length().toInt()
|
||||||
|
|
||||||
|
newImageAttributes = NewImageAttributes(
|
||||||
|
options.outWidth,
|
||||||
|
options.outHeight,
|
||||||
|
fileSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO Fix here the image rotation issue
|
||||||
|
originalFile
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Other type
|
||||||
|
originalFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var uploadedThumbnailUrl: String? = null
|
var uploadedThumbnailUrl: String? = null
|
||||||
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
||||||
@ -88,14 +131,14 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData ->
|
ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData ->
|
||||||
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
|
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
|
||||||
override fun onProgress(current: Long, total: Long) {
|
override fun onProgress(current: Long, total: Long) {
|
||||||
contentUploadStateTracker.setProgressThumbnail(eventId, current, total)
|
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val contentUploadResponse = if (params.isRoomEncrypted) {
|
val contentUploadResponse = if (params.isRoomEncrypted) {
|
||||||
Timber.v("Encrypt thumbnail")
|
Timber.v("Encrypt thumbnail")
|
||||||
contentUploadStateTracker.setEncryptingThumbnail(eventId)
|
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
|
||||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||||
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||||
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
||||||
@ -118,10 +161,12 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
|
|
||||||
val progressListener = object : ProgressRequestBody.Listener {
|
val progressListener = object : ProgressRequestBody.Listener {
|
||||||
override fun onProgress(current: Long, total: Long) {
|
override fun onProgress(current: Long, total: Long) {
|
||||||
if (isStopped) {
|
notifyTracker(params) {
|
||||||
contentUploadStateTracker.setFailure(eventId, Throwable("Cancelled"))
|
if (isStopped) {
|
||||||
} else {
|
contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
|
||||||
contentUploadStateTracker.setProgress(eventId, current, total)
|
} else {
|
||||||
|
contentUploadStateTracker.setProgress(it, current, total)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,7 +176,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
return try {
|
return try {
|
||||||
val contentUploadResponse = if (params.isRoomEncrypted) {
|
val contentUploadResponse = if (params.isRoomEncrypted) {
|
||||||
Timber.v("Encrypt file")
|
Timber.v("Encrypt file")
|
||||||
contentUploadStateTracker.setEncrypting(eventId)
|
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
|
||||||
|
|
||||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
|
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
|
||||||
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||||
@ -143,7 +188,12 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
|
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSuccess(params, contentUploadResponse.contentUri, uploadedFileEncryptedFileInfo, uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo)
|
handleSuccess(params,
|
||||||
|
contentUploadResponse.contentUri,
|
||||||
|
uploadedFileEncryptedFileInfo,
|
||||||
|
uploadedThumbnailUrl,
|
||||||
|
uploadedThumbnailEncryptedFileInfo,
|
||||||
|
newImageAttributes)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Timber.e(t)
|
Timber.e(t)
|
||||||
handleFailure(params, t)
|
handleFailure(params, t)
|
||||||
@ -151,7 +201,8 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFailure(params: Params, failure: Throwable): Result {
|
private fun handleFailure(params: Params, failure: Throwable): Result {
|
||||||
contentUploadStateTracker.setFailure(params.event.eventId!!, failure)
|
notifyTracker(params) { contentUploadStateTracker.setFailure(it, failure) }
|
||||||
|
|
||||||
return Result.success(
|
return Result.success(
|
||||||
WorkerParamsFactory.toData(
|
WorkerParamsFactory.toData(
|
||||||
params.copy(
|
params.copy(
|
||||||
@ -165,11 +216,17 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
attachmentUrl: String,
|
attachmentUrl: String,
|
||||||
encryptedFileInfo: EncryptedFileInfo?,
|
encryptedFileInfo: EncryptedFileInfo?,
|
||||||
thumbnailUrl: String?,
|
thumbnailUrl: String?,
|
||||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?): Result {
|
thumbnailEncryptedFileInfo: EncryptedFileInfo?,
|
||||||
|
newImageAttributes: NewImageAttributes?): Result {
|
||||||
Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped")
|
Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped")
|
||||||
contentUploadStateTracker.setSuccess(params.event.eventId!!)
|
notifyTracker(params) { contentUploadStateTracker.setSuccess(it) }
|
||||||
val event = updateEvent(params.event, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
|
|
||||||
val sendParams = SendEventWorker.Params(params.sessionId, params.roomId, event)
|
val updatedEvents = params.events
|
||||||
|
.map {
|
||||||
|
updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isRoomEncrypted)
|
||||||
return Result.success(WorkerParamsFactory.toData(sendParams))
|
return Result.success(WorkerParamsFactory.toData(sendParams))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,10 +234,11 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
url: String,
|
url: String,
|
||||||
encryptedFileInfo: EncryptedFileInfo?,
|
encryptedFileInfo: EncryptedFileInfo?,
|
||||||
thumbnailUrl: String? = null,
|
thumbnailUrl: String? = null,
|
||||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?): Event {
|
thumbnailEncryptedFileInfo: EncryptedFileInfo?,
|
||||||
|
newImageAttributes: NewImageAttributes?): Event {
|
||||||
val messageContent: MessageContent = event.content.toModel() ?: return event
|
val messageContent: MessageContent = event.content.toModel() ?: return event
|
||||||
val updatedContent = when (messageContent) {
|
val updatedContent = when (messageContent) {
|
||||||
is MessageImageContent -> messageContent.update(url, encryptedFileInfo)
|
is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newImageAttributes)
|
||||||
is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
|
is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
|
||||||
is MessageFileContent -> messageContent.update(url, encryptedFileInfo)
|
is MessageFileContent -> messageContent.update(url, encryptedFileInfo)
|
||||||
is MessageAudioContent -> messageContent.update(url, encryptedFileInfo)
|
is MessageAudioContent -> messageContent.update(url, encryptedFileInfo)
|
||||||
@ -189,11 +247,23 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
return event.copy(content = updatedContent.toContent())
|
return event.copy(content = updatedContent.toContent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun notifyTracker(params: Params, function: (String) -> Unit) {
|
||||||
|
params.events
|
||||||
|
.mapNotNull { it.eventId }
|
||||||
|
.forEach { eventId -> function.invoke(eventId) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun MessageImageContent.update(url: String,
|
private fun MessageImageContent.update(url: String,
|
||||||
encryptedFileInfo: EncryptedFileInfo?): MessageImageContent {
|
encryptedFileInfo: EncryptedFileInfo?,
|
||||||
|
newImageAttributes: NewImageAttributes?): MessageImageContent {
|
||||||
return copy(
|
return copy(
|
||||||
url = if (encryptedFileInfo == null) url else null,
|
url = if (encryptedFileInfo == null) url else null,
|
||||||
encryptedFileInfo = encryptedFileInfo?.copy(url = url)
|
encryptedFileInfo = encryptedFileInfo?.copy(url = url),
|
||||||
|
info = info?.copy(
|
||||||
|
width = newImageAttributes?.newWidth ?: info.width,
|
||||||
|
height = newImageAttributes?.newHeight ?: info.height,
|
||||||
|
size = newImageAttributes?.newFileSize ?: info.size
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,4 +296,8 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
encryptedFileInfo = encryptedFileInfo?.copy(url = url)
|
encryptedFileInfo = encryptedFileInfo?.copy(url = url)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_IMAGE_SIZE = 640
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,13 +196,13 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun createEncryptEventWork(event: Event, keepKeys: List<String>?): OneTimeWorkRequest {
|
private fun createEncryptEventWork(event: Event, keepKeys: List<String>?): OneTimeWorkRequest {
|
||||||
// Same parameter
|
// Same parameter
|
||||||
val params = EncryptEventWorker.Params(sessionId, roomId, event, keepKeys)
|
val params = EncryptEventWorker.Params(sessionId, event, keepKeys)
|
||||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||||
return timeLineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true)
|
return timeLineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event)
|
val sendContentWorkerParams = SendEventWorker.Params(sessionId, event)
|
||||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ import androidx.work.OneTimeWorkRequest
|
|||||||
import androidx.work.Operation
|
import androidx.work.Operation
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
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.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
@ -49,7 +48,6 @@ import java.util.concurrent.Executors
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
private const val UPLOAD_WORK = "UPLOAD_WORK"
|
private const val UPLOAD_WORK = "UPLOAD_WORK"
|
||||||
private const val BACKOFF_DELAY = 10_000L
|
|
||||||
|
|
||||||
internal class DefaultSendService @AssistedInject constructor(
|
internal class DefaultSendService @AssistedInject constructor(
|
||||||
@Assisted private val roomId: String,
|
@Assisted private val roomId: String,
|
||||||
@ -58,7 +56,6 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
@SessionId private val sessionId: String,
|
@SessionId private val sessionId: String,
|
||||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val monarchy: Monarchy,
|
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val localEchoRepository: LocalEchoRepository
|
private val localEchoRepository: LocalEchoRepository
|
||||||
) : SendService {
|
) : SendService {
|
||||||
@ -103,6 +100,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
return if (cryptoService.isRoomEncrypted(roomId)) {
|
return if (cryptoService.isRoomEncrypted(roomId)) {
|
||||||
Timber.v("Send event in encrypted room")
|
Timber.v("Send event in encrypted room")
|
||||||
val encryptWork = createEncryptEventWork(event, true)
|
val encryptWork = createEncryptEventWork(event, true)
|
||||||
|
// Note that event will be replaced by the result of the previous work
|
||||||
val sendWork = createSendEventWork(event, false)
|
val sendWork = createSendEventWork(event, false)
|
||||||
timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
|
timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
|
||||||
} else {
|
} else {
|
||||||
@ -111,9 +109,11 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable {
|
override fun sendMedias(attachments: List<ContentAttachmentData>,
|
||||||
|
compressBeforeSending: Boolean,
|
||||||
|
roomIds: Set<String>): Cancelable {
|
||||||
return attachments.mapTo(CancelableBag()) {
|
return attachments.mapTo(CancelableBag()) {
|
||||||
sendMedia(it)
|
sendMedia(it, compressBeforeSending, roomIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,43 +201,56 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
|
override fun sendMedia(attachment: ContentAttachmentData,
|
||||||
|
compressBeforeSending: Boolean,
|
||||||
|
roomIds: Set<String>): Cancelable {
|
||||||
// Create an event with the media file path
|
// Create an event with the media file path
|
||||||
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
|
// Ensure current roomId is included in the set
|
||||||
createLocalEcho(it)
|
val allRoomIds = (roomIds + roomId).toList()
|
||||||
|
|
||||||
|
// Create local echo for each room
|
||||||
|
val allLocalEchoes = allRoomIds.map {
|
||||||
|
localEchoEventFactory.createMediaEvent(it, attachment).also { event ->
|
||||||
|
createLocalEcho(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return internalSendMedia(event, attachment)
|
return internalSendMedia(allLocalEchoes, attachment, compressBeforeSending)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): Cancelable {
|
/**
|
||||||
val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId)
|
* We use the roomId of the local echo event
|
||||||
|
*/
|
||||||
|
private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
|
||||||
|
val cancelableBag = CancelableBag()
|
||||||
|
|
||||||
val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true)
|
allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) }
|
||||||
val sendWork = createSendEventWork(localEcho, false)
|
.apply {
|
||||||
|
keys.forEach { isRoomEncrypted ->
|
||||||
|
// Should never be empty
|
||||||
|
val localEchoes = get(isRoomEncrypted).orEmpty()
|
||||||
|
val uploadWork = createUploadMediaWork(localEchoes, attachment, isRoomEncrypted, compressBeforeSending, startChain = true)
|
||||||
|
|
||||||
if (isRoomEncrypted) {
|
val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted)
|
||||||
val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/)
|
|
||||||
|
|
||||||
val op: Operation = workManagerProvider.workManager
|
workManagerProvider.workManager
|
||||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||||
.then(encryptWork)
|
.then(dispatcherWork)
|
||||||
.then(sendWork)
|
.enqueue()
|
||||||
.enqueue()
|
.also { operation ->
|
||||||
op.result.addListener(Runnable {
|
operation.result.addListener(Runnable {
|
||||||
if (op.result.isCancelled) {
|
if (operation.result.isCancelled) {
|
||||||
Timber.e("CHAIN WAS CANCELLED")
|
Timber.e("CHAIN WAS CANCELLED")
|
||||||
} else if (op.state.value is Operation.State.FAILURE) {
|
} else if (operation.state.value is Operation.State.FAILURE) {
|
||||||
Timber.e("CHAIN DID FAIL")
|
Timber.e("CHAIN DID FAIL")
|
||||||
|
}
|
||||||
|
}, workerFutureListenerExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelableBag.add(CancelableWork(workManagerProvider.workManager, dispatcherWork.id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, workerFutureListenerExecutor)
|
|
||||||
} else {
|
|
||||||
workManagerProvider.workManager
|
|
||||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
|
||||||
.then(sendWork)
|
|
||||||
.enqueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
return CancelableWork(workManagerProvider.workManager, sendWork.id)
|
return cancelableBag
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createLocalEcho(event: Event) {
|
private fun createLocalEcho(event: Event) {
|
||||||
@ -250,19 +263,19 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||||
// Same parameter
|
// Same parameter
|
||||||
val params = EncryptEventWorker.Params(sessionId, roomId, event)
|
val params = EncryptEventWorker.Params(sessionId, event)
|
||||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||||
|
|
||||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||||
.setConstraints(WorkManagerProvider.workConstraints)
|
.setConstraints(WorkManagerProvider.workConstraints)
|
||||||
.setInputData(sendWorkData)
|
.setInputData(sendWorkData)
|
||||||
.startChain(startChain)
|
.startChain(startChain)
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event)
|
val sendContentWorkerParams = SendEventWorker.Params(sessionId, event)
|
||||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
|
||||||
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||||
@ -277,18 +290,33 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
return timelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData, true)
|
return timelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUploadMediaWork(event: Event,
|
private fun createUploadMediaWork(allLocalEchos: List<Event>,
|
||||||
attachment: ContentAttachmentData,
|
attachment: ContentAttachmentData,
|
||||||
isRoomEncrypted: Boolean,
|
isRoomEncrypted: Boolean,
|
||||||
|
compressBeforeSending: Boolean,
|
||||||
startChain: Boolean): OneTimeWorkRequest {
|
startChain: Boolean): OneTimeWorkRequest {
|
||||||
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, roomId, event, attachment, isRoomEncrypted)
|
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, allLocalEchos, attachment, isRoomEncrypted, compressBeforeSending)
|
||||||
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
|
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
|
||||||
|
|
||||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>()
|
return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>()
|
||||||
.setConstraints(WorkManagerProvider.workConstraints)
|
.setConstraints(WorkManagerProvider.workConstraints)
|
||||||
.startChain(startChain)
|
.startChain(startChain)
|
||||||
.setInputData(uploadWorkData)
|
.setInputData(uploadWorkData)
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMultipleEventDispatcherWork(isRoomEncrypted: Boolean): OneTimeWorkRequest {
|
||||||
|
// the list of events will be replaced by the result of the media upload work
|
||||||
|
val params = MultipleEventSendingDispatcherWorker.Params(sessionId, emptyList(), isRoomEncrypted)
|
||||||
|
val workData = WorkerParamsFactory.toData(params)
|
||||||
|
|
||||||
|
return workManagerProvider.matrixOneTimeWorkRequestBuilder<MultipleEventSendingDispatcherWorker>()
|
||||||
|
// No constraint
|
||||||
|
// .setConstraints(WorkManagerProvider.workConstraints)
|
||||||
|
.startChain(false)
|
||||||
|
.setInputData(workData)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,9 +38,8 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class Params(
|
internal data class Params(
|
||||||
override val sessionId: String,
|
override val sessionId: String,
|
||||||
val roomId: String,
|
|
||||||
val event: Event,
|
val event: Event,
|
||||||
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
|
/** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */
|
||||||
val keepKeys: List<String>? = null,
|
val keepKeys: List<String>? = null,
|
||||||
override val lastFailureMessage: String? = null
|
override val lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
@ -52,7 +51,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||||||
Timber.v("Start Encrypt work")
|
Timber.v("Start Encrypt work")
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.success().also {
|
?: return Result.success().also {
|
||||||
Timber.v("Work cancelled due to input error from parent")
|
Timber.e("Work cancelled due to input error from parent")
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.v("Start Encrypt work for event ${params.event.eventId}")
|
Timber.v("Start Encrypt work for event ${params.event.eventId}")
|
||||||
@ -79,7 +78,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||||||
var result: MXEncryptEventContentResult? = null
|
var result: MXEncryptEventContentResult? = null
|
||||||
try {
|
try {
|
||||||
result = awaitCallback {
|
result = awaitCallback {
|
||||||
crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
|
crypto.encryptEventContent(localMutableContent, localEvent.type, localEvent.roomId!!, it)
|
||||||
}
|
}
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
error = throwable
|
error = throwable
|
||||||
@ -97,7 +96,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||||||
type = safeResult.eventType,
|
type = safeResult.eventType,
|
||||||
content = safeResult.eventContent
|
content = safeResult.eventContent
|
||||||
)
|
)
|
||||||
val nextWorkerParams = SendEventWorker.Params(params.sessionId, params.roomId, encryptedEvent)
|
val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent)
|
||||||
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
||||||
} else {
|
} else {
|
||||||
val sendState = when (error) {
|
val sendState = when (error) {
|
||||||
@ -106,7 +105,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||||||
}
|
}
|
||||||
localEchoUpdater.updateSendState(localEvent.eventId, sendState)
|
localEchoUpdater.updateSendState(localEvent.eventId, sendState)
|
||||||
// always return success, or the chain will be stuck for ever!
|
// always return success, or the chain will be stuck for ever!
|
||||||
val nextWorkerParams = SendEventWorker.Params(params.sessionId, params.roomId, localEvent, error?.localizedMessage
|
val nextWorkerParams = SendEventWorker.Params(params.sessionId, localEvent, error?.localizedMessage
|
||||||
?: "Error")
|
?: "Error")
|
||||||
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.room.send
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.BackoffPolicy
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||||
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
|
||||||
|
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
||||||
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
|
import im.vector.matrix.android.internal.worker.startChain
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This worker creates a new work for each events passed in parameter
|
||||||
|
*/
|
||||||
|
internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters)
|
||||||
|
: CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class Params(
|
||||||
|
override val sessionId: String,
|
||||||
|
val events: List<Event>,
|
||||||
|
val isEncrypted: Boolean,
|
||||||
|
override val lastFailureMessage: String? = null
|
||||||
|
) : SessionWorkerParams
|
||||||
|
|
||||||
|
@Inject lateinit var workManagerProvider: WorkManagerProvider
|
||||||
|
@Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
Timber.v("Start dispatch sending multiple event work")
|
||||||
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
|
?: return Result.success().also {
|
||||||
|
Timber.e("Work cancelled due to input error from parent")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.lastFailureMessage != null) {
|
||||||
|
// Transmit the error
|
||||||
|
return Result.success(inputData)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
|
sessionComponent.inject(this)
|
||||||
|
|
||||||
|
// Create a work for every event
|
||||||
|
params.events.forEach { event ->
|
||||||
|
if (params.isEncrypted) {
|
||||||
|
Timber.v("Send event in encrypted room")
|
||||||
|
val encryptWork = createEncryptEventWork(params.sessionId, event, true)
|
||||||
|
// Note that event will be replaced by the result of the previous work
|
||||||
|
val sendWork = createSendEventWork(params.sessionId, event, false)
|
||||||
|
timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork)
|
||||||
|
} else {
|
||||||
|
val sendWork = createSendEventWork(params.sessionId, event, true)
|
||||||
|
timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createEncryptEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||||
|
val params = EncryptEventWorker.Params(sessionId, event)
|
||||||
|
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||||
|
|
||||||
|
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||||
|
.setConstraints(WorkManagerProvider.workConstraints)
|
||||||
|
.setInputData(sendWorkData)
|
||||||
|
.startChain(startChain)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSendEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||||
|
val sendContentWorkerParams = SendEventWorker.Params(sessionId, event)
|
||||||
|
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
|
||||||
|
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,6 @@ import androidx.work.CoroutineWorker
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.failure.shouldBeRetried
|
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
|
||||||
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.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
@ -30,6 +29,7 @@ import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
|||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class SendEventWorker(context: Context,
|
internal class SendEventWorker(context: Context,
|
||||||
@ -39,7 +39,6 @@ internal class SendEventWorker(context: Context,
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class Params(
|
internal data class Params(
|
||||||
override val sessionId: String,
|
override val sessionId: String,
|
||||||
val roomId: String,
|
|
||||||
val event: Event,
|
val event: Event,
|
||||||
override val lastFailureMessage: String? = null
|
override val lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
@ -50,7 +49,9 @@ internal class SendEventWorker(context: Context,
|
|||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.success()
|
?: return Result.success().also {
|
||||||
|
Timber.e("Work cancelled due to input error from parent")
|
||||||
|
}
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
@ -66,7 +67,7 @@ internal class SendEventWorker(context: Context,
|
|||||||
return Result.success(inputData)
|
return Result.success(inputData)
|
||||||
}
|
}
|
||||||
return try {
|
return try {
|
||||||
sendEvent(event.eventId, event.type, event.content, params.roomId)
|
sendEvent(event)
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
if (exception.shouldBeRetried()) {
|
if (exception.shouldBeRetried()) {
|
||||||
@ -79,16 +80,16 @@ internal class SendEventWorker(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) {
|
private suspend fun sendEvent(event: Event) {
|
||||||
localEchoUpdater.updateSendState(eventId, SendState.SENDING)
|
localEchoUpdater.updateSendState(event.eventId!!, SendState.SENDING)
|
||||||
executeRequest<SendResponse>(eventBus) {
|
executeRequest<SendResponse>(eventBus) {
|
||||||
apiCall = roomAPI.send(
|
apiCall = roomAPI.send(
|
||||||
eventId,
|
event.eventId,
|
||||||
roomId,
|
event.roomId!!,
|
||||||
eventType,
|
event.type,
|
||||||
content
|
event.content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
localEchoUpdater.updateSendState(eventId, SendState.SENT)
|
localEchoUpdater.updateSendState(event.eventId, SendState.SENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -341,6 +341,7 @@ dependencies {
|
|||||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||||
implementation 'com.danikula:videocache:2.7.1'
|
implementation 'com.danikula:videocache:2.7.1'
|
||||||
|
implementation 'com.github.yalantis:ucrop:2.2.4'
|
||||||
|
|
||||||
// Badge for compatibility
|
// Badge for compatibility
|
||||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||||
|
@ -88,7 +88,13 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".features.share.IncomingShareActivity">
|
<activity
|
||||||
|
android:name=".features.share.IncomingShareActivity"
|
||||||
|
android:parentActivityName=".features.home.HomeActivity">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value=".features.home.HomeActivity" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<data android:mimeType="*/*" />
|
<data android:mimeType="*/*" />
|
||||||
@ -134,6 +140,14 @@
|
|||||||
|
|
||||||
<activity android:name=".features.qrcode.QrCodeScannerActivity" />
|
<activity android:name=".features.qrcode.QrCodeScannerActivity" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="com.yalantis.ucrop.UCropActivity"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
|
||||||
|
android:theme="@style/AppTheme.AttachmentsPreview" />
|
||||||
|
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
@ -315,6 +315,11 @@ SOFTWARE.
|
|||||||
<br/>
|
<br/>
|
||||||
Copyright (c) 2012-2016 Dan Wheeler and Dropbox, Inc.
|
Copyright (c) 2012-2016 Dan Wheeler and Dropbox, Inc.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Compressor</b>
|
||||||
|
<br/>
|
||||||
|
Copyright (c) 2016 Zetra.
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<b>com.otaliastudios:autocomplete</b>
|
<b>com.otaliastudios:autocomplete</b>
|
||||||
<br/>
|
<br/>
|
||||||
@ -375,6 +380,11 @@ SOFTWARE.
|
|||||||
<br/>
|
<br/>
|
||||||
Copyright (c) 2014 Dushyanth Maguluru
|
Copyright (c) 2014 Dushyanth Maguluru
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>uCrop</b>
|
||||||
|
<br/>
|
||||||
|
Copyright 2017, Yalantis
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<b>BillCarsonFr/JsonViewer</b>
|
<b>BillCarsonFr/JsonViewer</b>
|
||||||
</li>
|
</li>
|
||||||
|
@ -22,6 +22,7 @@ import androidx.fragment.app.FragmentFactory
|
|||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.multibindings.IntoMap
|
import dagger.multibindings.IntoMap
|
||||||
|
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
|
||||||
import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
|
import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
|
||||||
import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
|
import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
||||||
@ -75,6 +76,7 @@ import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
|
|||||||
import im.vector.riotx.features.settings.devtools.AccountDataFragment
|
import im.vector.riotx.features.settings.devtools.AccountDataFragment
|
||||||
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
|
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
|
||||||
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
||||||
|
import im.vector.riotx.features.share.IncomingShareFragment
|
||||||
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
|
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ -350,6 +352,16 @@ interface FragmentModule {
|
|||||||
@FragmentKey(CrossSigningSettingsFragment::class)
|
@FragmentKey(CrossSigningSettingsFragment::class)
|
||||||
fun bindCrossSigningSettingsFragment(fragment: CrossSigningSettingsFragment): Fragment
|
fun bindCrossSigningSettingsFragment(fragment: CrossSigningSettingsFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(AttachmentsPreviewFragment::class)
|
||||||
|
fun bindAttachmentsPreviewFragment(fragment: AttachmentsPreviewFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(IncomingShareFragment::class)
|
||||||
|
fun bindIncomingShareFragment(fragment: IncomingShareFragment): Fragment
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(AccountDataFragment::class)
|
@FragmentKey(AccountDataFragment::class)
|
||||||
|
@ -47,7 +47,6 @@ import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
|
|||||||
import im.vector.riotx.features.reactions.data.EmojiDataSource
|
import im.vector.riotx.features.reactions.data.EmojiDataSource
|
||||||
import im.vector.riotx.features.session.SessionListener
|
import im.vector.riotx.features.session.SessionListener
|
||||||
import im.vector.riotx.features.settings.VectorPreferences
|
import im.vector.riotx.features.settings.VectorPreferences
|
||||||
import im.vector.riotx.features.share.ShareRoomListDataSource
|
|
||||||
import im.vector.riotx.features.ui.UiStateRepository
|
import im.vector.riotx.features.ui.UiStateRepository
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@ -97,8 +96,6 @@ interface VectorComponent {
|
|||||||
|
|
||||||
fun homeRoomListObservableStore(): HomeRoomListDataSource
|
fun homeRoomListObservableStore(): HomeRoomListDataSource
|
||||||
|
|
||||||
fun shareRoomListObservableStore(): ShareRoomListDataSource
|
|
||||||
|
|
||||||
fun selectedGroupStore(): SelectedGroupDataSource
|
fun selectedGroupStore(): SelectedGroupDataSource
|
||||||
|
|
||||||
fun activeSessionObservableStore(): ActiveSessionDataSource
|
fun activeSessionObservableStore(): ActiveSessionDataSource
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.core.platform
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.Checkable
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
|
||||||
|
class CheckableImageView : AppCompatImageView, Checkable {
|
||||||
|
|
||||||
|
private var mChecked = false
|
||||||
|
|
||||||
|
constructor(context: Context) : super(context)
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
|
override fun isChecked(): Boolean {
|
||||||
|
return mChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setChecked(b: Boolean) {
|
||||||
|
if (b != mChecked) {
|
||||||
|
mChecked = b
|
||||||
|
refreshDrawableState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toggle() {
|
||||||
|
isChecked = !mChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDrawableState(extraSpace: Int): IntArray {
|
||||||
|
val drawableState = super.onCreateDrawableState(extraSpace + 1)
|
||||||
|
if (isChecked) {
|
||||||
|
mergeDrawableStates(drawableState, CHECKED_STATE_SET)
|
||||||
|
}
|
||||||
|
return drawableState
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
|
||||||
|
}
|
||||||
|
}
|
@ -263,6 +263,9 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This should be provided by the framework
|
||||||
|
protected fun invalidateOptionsMenu() = requireActivity().invalidateOptionsMenu()
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* Common Dialogs
|
* Common Dialogs
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
@ -26,6 +26,7 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
class ColorProvider @Inject constructor(private val context: Context) {
|
class ColorProvider @Inject constructor(private val context: Context) {
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
fun getColor(@ColorRes colorRes: Int): Int {
|
fun getColor(@ColorRes colorRes: Int): Int {
|
||||||
return ContextCompat.getColor(context, colorRes)
|
return ContextCompat.getColor(context, colorRes)
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,7 @@ const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
|
|||||||
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
|
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
|
||||||
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
|
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
|
||||||
const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577
|
const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577
|
||||||
|
const val PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT = 578
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log the used permissions statuses.
|
* Log the used permissions statuses.
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.core.utils
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SnapHelper
|
||||||
|
|
||||||
|
interface OnSnapPositionChangeListener {
|
||||||
|
|
||||||
|
fun onSnapPositionChange(position: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RecyclerView.attachSnapHelperWithListener(
|
||||||
|
snapHelper: SnapHelper,
|
||||||
|
behavior: SnapOnScrollListener.Behavior = SnapOnScrollListener.Behavior.NOTIFY_ON_SCROLL_STATE_IDLE,
|
||||||
|
onSnapPositionChangeListener: OnSnapPositionChangeListener) {
|
||||||
|
snapHelper.attachToRecyclerView(this)
|
||||||
|
val snapOnScrollListener = SnapOnScrollListener(snapHelper, behavior, onSnapPositionChangeListener)
|
||||||
|
addOnScrollListener(snapOnScrollListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SnapHelper.getSnapPosition(recyclerView: RecyclerView): Int {
|
||||||
|
val layoutManager = recyclerView.layoutManager ?: return RecyclerView.NO_POSITION
|
||||||
|
val snapView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
|
||||||
|
return layoutManager.getPosition(snapView)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnapOnScrollListener(
|
||||||
|
private val snapHelper: SnapHelper,
|
||||||
|
var behavior: Behavior = Behavior.NOTIFY_ON_SCROLL,
|
||||||
|
var onSnapPositionChangeListener: OnSnapPositionChangeListener? = null
|
||||||
|
) : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
|
enum class Behavior {
|
||||||
|
NOTIFY_ON_SCROLL,
|
||||||
|
NOTIFY_ON_SCROLL_STATE_IDLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private var snapPosition = RecyclerView.NO_POSITION
|
||||||
|
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
if (behavior == Behavior.NOTIFY_ON_SCROLL) {
|
||||||
|
maybeNotifySnapPositionChange(recyclerView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
if (behavior == Behavior.NOTIFY_ON_SCROLL_STATE_IDLE
|
||||||
|
&& newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||||
|
maybeNotifySnapPositionChange(recyclerView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeNotifySnapPositionChange(recyclerView: RecyclerView) {
|
||||||
|
val snapPosition = snapHelper.getSnapPosition(recyclerView)
|
||||||
|
val snapPositionChanged = this.snapPosition != snapPosition
|
||||||
|
if (snapPositionChanged) {
|
||||||
|
onSnapPositionChangeListener?.onSnapPositionChange(snapPosition)
|
||||||
|
this.snapPosition = snapPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,11 +20,16 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.kbeanie.multipicker.api.Picker.*
|
import com.kbeanie.multipicker.api.Picker.PICK_AUDIO
|
||||||
|
import com.kbeanie.multipicker.api.Picker.PICK_CONTACT
|
||||||
|
import com.kbeanie.multipicker.api.Picker.PICK_FILE
|
||||||
|
import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_CAMERA
|
||||||
|
import com.kbeanie.multipicker.api.Picker.PICK_IMAGE_DEVICE
|
||||||
import com.kbeanie.multipicker.core.PickerManager
|
import com.kbeanie.multipicker.core.PickerManager
|
||||||
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
|
||||||
|
import im.vector.riotx.features.attachments.AttachmentsHelper.Callback
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
|
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
|
||||||
|
@ -16,7 +16,11 @@
|
|||||||
|
|
||||||
package im.vector.riotx.features.attachments
|
package im.vector.riotx.features.attachments
|
||||||
|
|
||||||
import com.kbeanie.multipicker.api.entity.*
|
import com.kbeanie.multipicker.api.entity.ChosenAudio
|
||||||
|
import com.kbeanie.multipicker.api.entity.ChosenContact
|
||||||
|
import com.kbeanie.multipicker.api.entity.ChosenFile
|
||||||
|
import com.kbeanie.multipicker.api.entity.ChosenImage
|
||||||
|
import com.kbeanie.multipicker.api.entity.ChosenVideo
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ -37,7 +41,8 @@ fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
|
|||||||
type = mapType(),
|
type = mapType(),
|
||||||
size = size,
|
size = size,
|
||||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||||
name = displayName
|
name = displayName,
|
||||||
|
queryUri = queryUri
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +55,8 @@ fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
|
|||||||
size = size,
|
size = size,
|
||||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||||
name = displayName,
|
name = displayName,
|
||||||
duration = duration
|
duration = duration,
|
||||||
|
queryUri = queryUri
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +80,8 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
|
|||||||
height = height.toLong(),
|
height = height.toLong(),
|
||||||
width = width.toLong(),
|
width = width.toLong(),
|
||||||
exifOrientation = orientation,
|
exifOrientation = orientation,
|
||||||
date = createdAt?.time ?: System.currentTimeMillis()
|
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||||
|
queryUri = queryUri
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +96,7 @@ fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
|
|||||||
height = height.toLong(),
|
height = height.toLong(),
|
||||||
width = width.toLong(),
|
width = width.toLong(),
|
||||||
duration = duration,
|
duration = duration,
|
||||||
name = displayName
|
name = displayName,
|
||||||
|
queryUri = queryUri
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,11 @@ import com.kbeanie.multipicker.api.callbacks.ContactPickerCallback
|
|||||||
import com.kbeanie.multipicker.api.callbacks.FilePickerCallback
|
import com.kbeanie.multipicker.api.callbacks.FilePickerCallback
|
||||||
import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback
|
import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback
|
||||||
import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback
|
import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback
|
||||||
import com.kbeanie.multipicker.api.entity.*
|
import com.kbeanie.multipicker.api.entity.ChosenAudio
|
||||||
|
import com.kbeanie.multipicker.api.entity.ChosenContact
|
||||||
|
import com.kbeanie.multipicker.api.entity.ChosenFile
|
||||||
|
import com.kbeanie.multipicker.api.entity.ChosenImage
|
||||||
|
import com.kbeanie.multipicker.api.entity.ChosenVideo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback]
|
* This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback]
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.attachments
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
|
||||||
|
fun ContentAttachmentData.isPreviewable(): Boolean {
|
||||||
|
return type == ContentAttachmentData.Type.IMAGE || type == ContentAttachmentData.Type.VIDEO
|
||||||
|
}
|
||||||
|
|
||||||
|
data class GroupedContentAttachmentData(
|
||||||
|
val previewables: List<ContentAttachmentData>,
|
||||||
|
val notPreviewables: List<ContentAttachmentData>
|
||||||
|
)
|
||||||
|
|
||||||
|
fun List<ContentAttachmentData>.toGroupedContentAttachmentData(): GroupedContentAttachmentData {
|
||||||
|
return groupBy { it.isPreviewable() }
|
||||||
|
.let {
|
||||||
|
GroupedContentAttachmentData(
|
||||||
|
it[true].orEmpty(),
|
||||||
|
it[false].orEmpty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.attachments.preview
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class AttachmentBigPreviewController @Inject constructor() : TypedEpoxyController<AttachmentsPreviewViewState>() {
|
||||||
|
|
||||||
|
override fun buildModels(data: AttachmentsPreviewViewState) {
|
||||||
|
data.attachments.forEach {
|
||||||
|
attachmentBigPreviewItem {
|
||||||
|
id(it.path)
|
||||||
|
attachment(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachmentMiniaturePreviewController @Inject constructor() : TypedEpoxyController<AttachmentsPreviewViewState>() {
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onAttachmentClicked(position: Int, contentAttachmentData: ContentAttachmentData)
|
||||||
|
}
|
||||||
|
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
override fun buildModels(data: AttachmentsPreviewViewState) {
|
||||||
|
data.attachments.forEachIndexed { index, contentAttachmentData ->
|
||||||
|
attachmentMiniaturePreviewItem {
|
||||||
|
id(contentAttachmentData.path)
|
||||||
|
attachment(contentAttachmentData)
|
||||||
|
checked(data.currentAttachmentIndex == index)
|
||||||
|
clickListener { _ ->
|
||||||
|
callback?.onAttachmentClicked(index, contentAttachmentData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.attachments.preview
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.riotx.core.platform.CheckableImageView
|
||||||
|
|
||||||
|
abstract class AttachmentPreviewItem<H : AttachmentPreviewItem.Holder> : VectorEpoxyModel<H>() {
|
||||||
|
|
||||||
|
abstract val attachment: ContentAttachmentData
|
||||||
|
|
||||||
|
override fun bind(holder: H) {
|
||||||
|
val path = attachment.path
|
||||||
|
if (attachment.type == ContentAttachmentData.Type.VIDEO || attachment.type == ContentAttachmentData.Type.IMAGE) {
|
||||||
|
Glide.with(holder.view.context)
|
||||||
|
.asBitmap()
|
||||||
|
.load(path)
|
||||||
|
.apply(RequestOptions().frame(0))
|
||||||
|
.into(holder.imageView)
|
||||||
|
} else {
|
||||||
|
holder.imageView.setImageResource(R.drawable.filetype_attachment)
|
||||||
|
holder.imageView.scaleType = ImageView.ScaleType.FIT_CENTER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Holder : VectorEpoxyHolder() {
|
||||||
|
abstract val imageView: ImageView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_attachment_miniature_preview)
|
||||||
|
abstract class AttachmentMiniaturePreviewItem : AttachmentPreviewItem<AttachmentMiniaturePreviewItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute override lateinit var attachment: ContentAttachmentData
|
||||||
|
@EpoxyAttribute
|
||||||
|
var clickListener: View.OnClickListener? = null
|
||||||
|
@EpoxyAttribute
|
||||||
|
var checked: Boolean = false
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
holder.imageView.isChecked = checked
|
||||||
|
holder.view.setOnClickListener(clickListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : AttachmentPreviewItem.Holder() {
|
||||||
|
override val imageView: CheckableImageView
|
||||||
|
get() = miniatureImageView
|
||||||
|
private val miniatureImageView by bind<CheckableImageView>(R.id.attachmentMiniatureImageView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_attachment_big_preview)
|
||||||
|
abstract class AttachmentBigPreviewItem : AttachmentPreviewItem<AttachmentBigPreviewItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute override lateinit var attachment: ContentAttachmentData
|
||||||
|
|
||||||
|
class Holder : AttachmentPreviewItem.Holder() {
|
||||||
|
override val imageView: ImageView
|
||||||
|
get() = bigImageView
|
||||||
|
private val bigImageView by bind<ImageView>(R.id.attachmentBigImageView)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.attachments.preview
|
||||||
|
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
sealed class AttachmentsPreviewAction : VectorViewModelAction {
|
||||||
|
object RemoveCurrentAttachment : AttachmentsPreviewAction()
|
||||||
|
data class SetCurrentAttachment(val index: Int): AttachmentsPreviewAction()
|
||||||
|
data class UpdatePathOfCurrentAttachment(val newPath: String): AttachmentsPreviewAction()
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.attachments.preview
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.addFragment
|
||||||
|
import im.vector.riotx.core.platform.ToolbarConfigurable
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
|
import im.vector.riotx.features.themes.ActivityOtherThemes
|
||||||
|
|
||||||
|
class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REQUEST_CODE = 55
|
||||||
|
|
||||||
|
private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS"
|
||||||
|
private const val ATTACHMENTS_PREVIEW_RESULT = "ATTACHMENTS_PREVIEW_RESULT"
|
||||||
|
private const val KEEP_ORIGINAL_IMAGES_SIZE = "KEEP_ORIGINAL_IMAGES_SIZE"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, args: AttachmentsPreviewArgs): Intent {
|
||||||
|
return Intent(context, AttachmentsPreviewActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_FRAGMENT_ARGS, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOutput(intent: Intent): List<ContentAttachmentData> {
|
||||||
|
return intent.getParcelableArrayListExtra(ATTACHMENTS_PREVIEW_RESULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getKeepOriginalSize(intent: Intent): Boolean {
|
||||||
|
return intent.getBooleanExtra(KEEP_ORIGINAL_IMAGES_SIZE, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOtherThemes() = ActivityOtherThemes.AttachmentsPreview
|
||||||
|
|
||||||
|
override fun getLayoutRes() = R.layout.activity_simple
|
||||||
|
|
||||||
|
override fun initUiAndData() {
|
||||||
|
if (isFirstCreation()) {
|
||||||
|
val fragmentArgs: AttachmentsPreviewArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS) ?: return
|
||||||
|
addFragment(R.id.simpleFragmentContainer, AttachmentsPreviewFragment::class.java, fragmentArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setResultAndFinish(data: List<ContentAttachmentData>, keepOriginalImageSize: Boolean) {
|
||||||
|
val resultIntent = Intent().apply {
|
||||||
|
putParcelableArrayListExtra(ATTACHMENTS_PREVIEW_RESULT, ArrayList(data))
|
||||||
|
putExtra(KEEP_ORIGINAL_IMAGES_SIZE, keepOriginalImageSize)
|
||||||
|
}
|
||||||
|
setResult(RESULT_OK, resultIntent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun configure(toolbar: Toolbar) {
|
||||||
|
configureToolbar(toolbar)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,255 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.attachments.preview
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_CANCELED
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.PagerSnapHelper
|
||||||
|
import com.airbnb.mvrx.args
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import com.yalantis.ucrop.UCrop
|
||||||
|
import com.yalantis.ucrop.UCropActivity
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.cleanup
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
|
import im.vector.riotx.core.utils.OnSnapPositionChangeListener
|
||||||
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
|
||||||
|
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT
|
||||||
|
import im.vector.riotx.core.utils.SnapOnScrollListener
|
||||||
|
import im.vector.riotx.core.utils.allGranted
|
||||||
|
import im.vector.riotx.core.utils.attachSnapHelperWithListener
|
||||||
|
import im.vector.riotx.core.utils.checkPermissions
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import kotlinx.android.synthetic.main.fragment_attachments_preview.*
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class AttachmentsPreviewArgs(
|
||||||
|
val attachments: List<ContentAttachmentData>
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
class AttachmentsPreviewFragment @Inject constructor(
|
||||||
|
val viewModelFactory: AttachmentsPreviewViewModel.Factory,
|
||||||
|
private val attachmentMiniaturePreviewController: AttachmentMiniaturePreviewController,
|
||||||
|
private val attachmentBigPreviewController: AttachmentBigPreviewController,
|
||||||
|
private val colorProvider: ColorProvider
|
||||||
|
) : VectorBaseFragment(), AttachmentMiniaturePreviewController.Callback {
|
||||||
|
|
||||||
|
private val fragmentArgs: AttachmentsPreviewArgs by args()
|
||||||
|
private val viewModel: AttachmentsPreviewViewModel by fragmentViewModel()
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.fragment_attachments_preview
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
applyInsets()
|
||||||
|
setupRecyclerViews()
|
||||||
|
setupToolbar(attachmentPreviewerToolbar)
|
||||||
|
attachmentPreviewerSendButton.setOnClickListener {
|
||||||
|
setResultAndFinish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
if (requestCode == UCrop.REQUEST_CROP && data != null) {
|
||||||
|
Timber.v("Crop success")
|
||||||
|
handleCropResult(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resultCode == UCrop.RESULT_ERROR) {
|
||||||
|
Timber.v("Crop error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.attachmentsPreviewRemoveAction -> {
|
||||||
|
handleRemoveAction()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.attachmentsPreviewEditAction -> {
|
||||||
|
handleEditAction()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
|
withState(viewModel) { state ->
|
||||||
|
val editMenuItem = menu.findItem(R.id.attachmentsPreviewEditAction)
|
||||||
|
val showEditMenuItem = state.attachments[state.currentAttachmentIndex].isEditable()
|
||||||
|
editMenuItem.setVisible(showEditMenuItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onPrepareOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMenuRes() = R.menu.vector_attachments_preview
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
attachmentPreviewerMiniatureList.cleanup()
|
||||||
|
attachmentPreviewerBigList.cleanup()
|
||||||
|
attachmentMiniaturePreviewController.callback = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
if (state.attachments.isEmpty()) {
|
||||||
|
requireActivity().setResult(RESULT_CANCELED)
|
||||||
|
requireActivity().finish()
|
||||||
|
} else {
|
||||||
|
attachmentMiniaturePreviewController.setData(state)
|
||||||
|
attachmentBigPreviewController.setData(state)
|
||||||
|
attachmentPreviewerBigList.scrollToPosition(state.currentAttachmentIndex)
|
||||||
|
attachmentPreviewerMiniatureList.scrollToPosition(state.currentAttachmentIndex)
|
||||||
|
attachmentPreviewerSendImageOriginalSize.text = resources.getQuantityString(R.plurals.send_images_with_original_size, state.attachments.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachmentClicked(position: Int, contentAttachmentData: ContentAttachmentData) {
|
||||||
|
viewModel.handle(AttachmentsPreviewAction.SetCurrentAttachment(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setResultAndFinish() = withState(viewModel) {
|
||||||
|
(requireActivity() as? AttachmentsPreviewActivity)?.setResultAndFinish(
|
||||||
|
it.attachments,
|
||||||
|
attachmentPreviewerSendImageOriginalSize.isChecked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyInsets() {
|
||||||
|
view?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(attachmentPreviewerBottomContainer) { v, insets ->
|
||||||
|
v.updatePadding(bottom = insets.systemWindowInsetBottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(attachmentPreviewerToolbar) { v, insets ->
|
||||||
|
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
topMargin = insets.systemWindowInsetTop
|
||||||
|
}
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCropResult(result: Intent) {
|
||||||
|
val resultPath = UCrop.getOutput(result)?.path
|
||||||
|
if (resultPath != null) {
|
||||||
|
viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultPath))
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRemoveAction() {
|
||||||
|
viewModel.handle(AttachmentsPreviewAction.RemoveCurrentAttachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEditAction() {
|
||||||
|
// check permissions
|
||||||
|
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT)) {
|
||||||
|
doHandleEditAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
|
||||||
|
if (requestCode == PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT && allGranted(grantResults)) {
|
||||||
|
doHandleEditAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doHandleEditAction() = withState(viewModel) {
|
||||||
|
val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState
|
||||||
|
val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}")
|
||||||
|
UCrop.of(currentAttachment.queryUri.toUri(), destinationFile.toUri())
|
||||||
|
.withOptions(
|
||||||
|
UCrop.Options()
|
||||||
|
.apply {
|
||||||
|
setAllowedGestures(
|
||||||
|
/* tabScale = */ UCropActivity.SCALE,
|
||||||
|
/* tabRotate = */ UCropActivity.ALL,
|
||||||
|
/* tabAspectRatio = */ UCropActivity.SCALE
|
||||||
|
)
|
||||||
|
setToolbarTitle(currentAttachment.name)
|
||||||
|
setFreeStyleCropEnabled(true)
|
||||||
|
// Color used for toolbar icon and text
|
||||||
|
setToolbarColor(colorProvider.getColorFromAttribute(R.attr.riotx_background))
|
||||||
|
setToolbarWidgetColor(colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_primary_text_color))
|
||||||
|
// Background
|
||||||
|
setRootViewBackgroundColor(colorProvider.getColorFromAttribute(R.attr.riotx_background))
|
||||||
|
// Status bar color (pb in dark mode, icon of the status bar are dark)
|
||||||
|
setStatusBarColor(colorProvider.getColorFromAttribute(R.attr.riotx_header_panel_background))
|
||||||
|
// Known issue: there is still orange color used by the lib
|
||||||
|
// https://github.com/Yalantis/uCrop/issues/602
|
||||||
|
setActiveControlsWidgetColor(colorProvider.getColor(R.color.riotx_accent))
|
||||||
|
// Hide the logo (does not work)
|
||||||
|
setLogoColor(Color.TRANSPARENT)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.start(requireContext(), this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerViews() {
|
||||||
|
attachmentMiniaturePreviewController.callback = this
|
||||||
|
|
||||||
|
attachmentPreviewerMiniatureList.let {
|
||||||
|
it.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
it.setHasFixedSize(true)
|
||||||
|
it.adapter = attachmentMiniaturePreviewController.adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentPreviewerBigList.let {
|
||||||
|
it.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
it.attachSnapHelperWithListener(
|
||||||
|
PagerSnapHelper(),
|
||||||
|
SnapOnScrollListener.Behavior.NOTIFY_ON_SCROLL_STATE_IDLE,
|
||||||
|
object : OnSnapPositionChangeListener {
|
||||||
|
override fun onSnapPositionChange(position: Int) {
|
||||||
|
viewModel.handle(AttachmentsPreviewAction.SetCurrentAttachment(position))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it.setHasFixedSize(true)
|
||||||
|
it.adapter = attachmentBigPreviewController.adapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 New Vector Ltd
|
* Copyright 2020 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -12,14 +12,11 @@
|
|||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.share
|
package im.vector.riotx.features.attachments.preview
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.riotx.core.platform.VectorViewEvents
|
||||||
import im.vector.riotx.core.utils.BehaviorDataSource
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
sealed class AttachmentsPreviewViewEvents : VectorViewEvents
|
||||||
class ShareRoomListDataSource @Inject constructor() : BehaviorDataSource<List<RoomSummary>>()
|
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.attachments.preview
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import com.squareup.inject.assisted.Assisted
|
||||||
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
|
||||||
|
class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialState: AttachmentsPreviewViewState)
|
||||||
|
: VectorViewModel<AttachmentsPreviewViewState, AttachmentsPreviewAction, AttachmentsPreviewViewEvents>(initialState) {
|
||||||
|
|
||||||
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: AttachmentsPreviewViewState): AttachmentsPreviewViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<AttachmentsPreviewViewModel, AttachmentsPreviewViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: AttachmentsPreviewViewState): AttachmentsPreviewViewModel? {
|
||||||
|
val fragment: AttachmentsPreviewFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
|
return fragment.viewModelFactory.create(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: AttachmentsPreviewAction) {
|
||||||
|
when (action) {
|
||||||
|
is AttachmentsPreviewAction.SetCurrentAttachment -> handleSetCurrentAttachment(action)
|
||||||
|
is AttachmentsPreviewAction.UpdatePathOfCurrentAttachment -> handleUpdatePathOfCurrentAttachment(action)
|
||||||
|
AttachmentsPreviewAction.RemoveCurrentAttachment -> handleRemoveCurrentAttachment()
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRemoveCurrentAttachment() = withState {
|
||||||
|
val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState
|
||||||
|
val attachments = it.attachments.minusElement(currentAttachment)
|
||||||
|
val newAttachmentIndex = it.currentAttachmentIndex.coerceAtMost(attachments.size - 1)
|
||||||
|
setState {
|
||||||
|
copy(attachments = attachments, currentAttachmentIndex = newAttachmentIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUpdatePathOfCurrentAttachment(action: AttachmentsPreviewAction.UpdatePathOfCurrentAttachment) = withState {
|
||||||
|
val attachments = it.attachments.mapIndexed { index, contentAttachmentData ->
|
||||||
|
if (index == it.currentAttachmentIndex) {
|
||||||
|
contentAttachmentData.copy(path = action.newPath)
|
||||||
|
} else {
|
||||||
|
contentAttachmentData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState {
|
||||||
|
copy(attachments = attachments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSetCurrentAttachment(action: AttachmentsPreviewAction.SetCurrentAttachment) = setState {
|
||||||
|
copy(currentAttachmentIndex = action.index)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.attachments.preview
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
|
||||||
|
data class AttachmentsPreviewViewState(
|
||||||
|
val attachments: List<ContentAttachmentData>,
|
||||||
|
val currentAttachmentIndex: Int = 0,
|
||||||
|
val sendImagesWithOriginalSize: Boolean = false
|
||||||
|
) : MvRxState {
|
||||||
|
|
||||||
|
constructor(args: AttachmentsPreviewArgs) : this(attachments = args.attachments)
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.attachments.preview
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All images are editable, expect Gif
|
||||||
|
*/
|
||||||
|
fun ContentAttachmentData.isEditable(): Boolean {
|
||||||
|
return type == ContentAttachmentData.Type.IMAGE
|
||||||
|
&& mimeType?.startsWith("image/") == true
|
||||||
|
&& mimeType != "image/gif"
|
||||||
|
}
|
@ -149,7 +149,7 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun renderSelectedUsers(selectedUsers: Set<User>) {
|
private fun renderSelectedUsers(selectedUsers: Set<User>) {
|
||||||
vectorBaseActivity.invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
if (selectedUsers.isNotEmpty() && chipGroup.size == 0) {
|
if (selectedUsers.isNotEmpty() && chipGroup.size == 0) {
|
||||||
selectedUsers.forEach { addChipToGroup(it, chipGroup) }
|
selectedUsers.forEach { addChipToGroup(it, chipGroup) }
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,5 @@ enum class RoomListDisplayMode(@StringRes val titleRes: Int) {
|
|||||||
HOME(R.string.bottom_action_home),
|
HOME(R.string.bottom_action_home),
|
||||||
PEOPLE(R.string.bottom_action_people_x),
|
PEOPLE(R.string.bottom_action_people_x),
|
||||||
ROOMS(R.string.bottom_action_rooms),
|
ROOMS(R.string.bottom_action_rooms),
|
||||||
FILTERED(/* Not used */ 0),
|
FILTERED(/* Not used */ 0)
|
||||||
SHARE(/* Not used */ 0)
|
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||||||
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
|
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
|
||||||
data class SaveDraft(val draft: String) : RoomDetailAction()
|
data class SaveDraft(val draft: String) : RoomDetailAction()
|
||||||
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction()
|
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction()
|
||||||
data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailAction()
|
data class SendMedia(val attachments: List<ContentAttachmentData>, val compressBeforeSending: Boolean) : RoomDetailAction()
|
||||||
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
|
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
|
||||||
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction()
|
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction()
|
||||||
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailAction()
|
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailAction()
|
||||||
|
@ -116,6 +116,9 @@ import im.vector.riotx.core.utils.toast
|
|||||||
import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
|
import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
|
||||||
import im.vector.riotx.features.attachments.AttachmentsHelper
|
import im.vector.riotx.features.attachments.AttachmentsHelper
|
||||||
import im.vector.riotx.features.attachments.ContactAttachment
|
import im.vector.riotx.features.attachments.ContactAttachment
|
||||||
|
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity
|
||||||
|
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs
|
||||||
|
import im.vector.riotx.features.attachments.toGroupedContentAttachmentData
|
||||||
import im.vector.riotx.features.command.Command
|
import im.vector.riotx.features.command.Command
|
||||||
import im.vector.riotx.features.crypto.util.toImageRes
|
import im.vector.riotx.features.crypto.util.toImageRes
|
||||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
||||||
@ -299,10 +302,15 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
when (val sharedData = roomDetailArgs.sharedData) {
|
when (val sharedData = roomDetailArgs.sharedData) {
|
||||||
is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false))
|
is SharedData.Text -> {
|
||||||
is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData))
|
roomDetailViewModel.handle(RoomDetailAction.ExitSpecialMode(composerLayout.text.toString()))
|
||||||
null -> Timber.v("No share data to process")
|
}
|
||||||
}
|
is SharedData.Attachments -> {
|
||||||
|
// open share edition
|
||||||
|
onContentAttachmentsReady(sharedData.attachmentData)
|
||||||
|
}
|
||||||
|
null -> Timber.v("No share data to process")
|
||||||
|
}.exhaustive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -498,7 +506,12 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
|
val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
|
||||||
if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
|
if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
REACTION_SELECT_REQUEST_CODE -> {
|
AttachmentsPreviewActivity.REQUEST_CODE -> {
|
||||||
|
val sendData = AttachmentsPreviewActivity.getOutput(data)
|
||||||
|
val keepOriginalSize = AttachmentsPreviewActivity.getKeepOriginalSize(data)
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.SendMedia(sendData, !keepOriginalSize))
|
||||||
|
}
|
||||||
|
REACTION_SELECT_REQUEST_CODE -> {
|
||||||
val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return
|
val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return
|
||||||
roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction))
|
roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction))
|
||||||
}
|
}
|
||||||
@ -637,9 +650,11 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun sendUri(uri: Uri): Boolean {
|
private fun sendUri(uri: Uri): Boolean {
|
||||||
|
roomDetailViewModel.preventAttachmentPreview = true
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
||||||
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
|
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
|
||||||
if (!isHandled) {
|
if (!isHandled) {
|
||||||
|
roomDetailViewModel.preventAttachmentPreview = false
|
||||||
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
return isHandled
|
return isHandled
|
||||||
@ -1334,10 +1349,24 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
// AttachmentsHelper.Callback
|
// AttachmentsHelper.Callback
|
||||||
|
|
||||||
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
|
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.SendMedia(attachments))
|
if (roomDetailViewModel.preventAttachmentPreview) {
|
||||||
|
roomDetailViewModel.preventAttachmentPreview = false
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.SendMedia(attachments, false))
|
||||||
|
} else {
|
||||||
|
val grouped = attachments.toGroupedContentAttachmentData()
|
||||||
|
if (grouped.notPreviewables.isNotEmpty()) {
|
||||||
|
// Send the not previewable attachments right now (?)
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.SendMedia(grouped.notPreviewables, false))
|
||||||
|
}
|
||||||
|
if (grouped.previewables.isNotEmpty()) {
|
||||||
|
val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(grouped.previewables))
|
||||||
|
startActivityForResult(intent, AttachmentsPreviewActivity.REQUEST_CODE)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachmentsProcessFailed() {
|
override fun onAttachmentsProcessFailed() {
|
||||||
|
roomDetailViewModel.preventAttachmentPreview = false
|
||||||
Toast.makeText(requireContext(), R.string.error_attachment, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.error_attachment, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,6 +115,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
var pendingAction: RoomDetailAction? = null
|
var pendingAction: RoomDetailAction? = null
|
||||||
// Slot to keep a pending uri during permission request
|
// Slot to keep a pending uri during permission request
|
||||||
var pendingUri: Uri? = null
|
var pendingUri: Uri? = null
|
||||||
|
// Slot to store if we want to prevent preview of attachment
|
||||||
|
var preventAttachmentPreview = false
|
||||||
|
|
||||||
private var trackUnreadMessages = AtomicBoolean(false)
|
private var trackUnreadMessages = AtomicBoolean(false)
|
||||||
private var mostRecentDisplayedEvent: TimelineEvent? = null
|
private var mostRecentDisplayedEvent: TimelineEvent? = null
|
||||||
@ -582,10 +584,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
|
|
||||||
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
|
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
|
||||||
// Unknown limitation
|
// Unknown limitation
|
||||||
room.sendMedias(attachments)
|
room.sendMedias(attachments, action.compressBeforeSending, emptySet())
|
||||||
} else {
|
} else {
|
||||||
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
||||||
null -> room.sendMedias(attachments)
|
null -> room.sendMedias(attachments, action.compressBeforeSending, emptySet())
|
||||||
else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError(
|
else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError(
|
||||||
tooBigFile.name ?: tooBigFile.path,
|
tooBigFile.name ?: tooBigFile.path,
|
||||||
tooBigFile.size,
|
tooBigFile.size,
|
||||||
|
@ -33,7 +33,6 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListDisplayMode) :
|
|||||||
RoomListDisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
|
RoomListDisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
|
||||||
RoomListDisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
|
RoomListDisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
|
||||||
RoomListDisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN
|
RoomListDisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN
|
||||||
RoomListDisplayMode.SHARE -> roomSummary.membership == Membership.JOIN
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,15 +50,13 @@ import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsShare
|
|||||||
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
|
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
|
||||||
import im.vector.riotx.features.home.room.list.widget.FabMenuView
|
import im.vector.riotx.features.home.room.list.widget.FabMenuView
|
||||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||||
import im.vector.riotx.features.share.SharedData
|
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.fragment_room_list.*
|
import kotlinx.android.synthetic.main.fragment_room_list.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class RoomListParams(
|
data class RoomListParams(
|
||||||
val displayMode: RoomListDisplayMode,
|
val displayMode: RoomListDisplayMode
|
||||||
val sharedData: SharedData? = null
|
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
class RoomListFragment @Inject constructor(
|
class RoomListFragment @Inject constructor(
|
||||||
@ -106,7 +104,7 @@ class RoomListFragment @Inject constructor(
|
|||||||
when (it) {
|
when (it) {
|
||||||
is RoomListViewEvents.Loading -> showLoading(it.message)
|
is RoomListViewEvents.Loading -> showLoading(it.message)
|
||||||
is RoomListViewEvents.Failure -> showFailure(it.throwable)
|
is RoomListViewEvents.Failure -> showFailure(it.throwable)
|
||||||
is RoomListViewEvents.SelectRoom -> openSelectedRoom(it)
|
is RoomListViewEvents.SelectRoom -> handleSelectRoom(it)
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,13 +129,8 @@ class RoomListFragment @Inject constructor(
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) {
|
private fun handleSelectRoom(event: RoomListViewEvents.SelectRoom) {
|
||||||
if (roomListParams.displayMode == RoomListDisplayMode.SHARE) {
|
navigator.openRoom(requireActivity(), event.roomSummary.roomId)
|
||||||
val sharedData = roomListParams.sharedData ?: return
|
|
||||||
navigator.openRoomForSharing(requireActivity(), event.roomId, sharedData)
|
|
||||||
} else {
|
|
||||||
navigator.openRoom(requireActivity(), event.roomId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupCreateRoomButton() {
|
private fun setupCreateRoomButton() {
|
||||||
@ -256,7 +249,6 @@ class RoomListFragment @Inject constructor(
|
|||||||
is Fail -> renderFailure(state.asyncFilteredRooms.error)
|
is Fail -> renderFailure(state.asyncFilteredRooms.error)
|
||||||
}
|
}
|
||||||
roomController.update(state)
|
roomController.update(state)
|
||||||
|
|
||||||
// Mark all as read menu
|
// Mark all as read menu
|
||||||
when (roomListParams.displayMode) {
|
when (roomListParams.displayMode) {
|
||||||
RoomListDisplayMode.HOME,
|
RoomListDisplayMode.HOME,
|
||||||
@ -265,7 +257,7 @@ class RoomListFragment @Inject constructor(
|
|||||||
val newValue = state.hasUnread
|
val newValue = state.hasUnread
|
||||||
if (hasUnreadRooms != newValue) {
|
if (hasUnreadRooms != newValue) {
|
||||||
hasUnreadRooms = newValue
|
hasUnreadRooms = newValue
|
||||||
requireActivity().invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
@ -338,7 +330,6 @@ class RoomListFragment @Inject constructor(
|
|||||||
if (createChatFabMenu.onBackPressed()) {
|
if (createChatFabMenu.onBackPressed()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,7 +341,6 @@ class RoomListFragment @Inject constructor(
|
|||||||
|
|
||||||
override fun onRoomLongClicked(room: RoomSummary): Boolean {
|
override fun onRoomLongClicked(room: RoomSummary): Boolean {
|
||||||
roomController.onRoomLongClicked()
|
roomController.onRoomLongClicked()
|
||||||
|
|
||||||
RoomListQuickActionsBottomSheet
|
RoomListQuickActionsBottomSheet
|
||||||
.newInstance(room.roomId, RoomListActionsArgs.Mode.FULL)
|
.newInstance(room.roomId, RoomListActionsArgs.Mode.FULL)
|
||||||
.show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS")
|
.show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS")
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package im.vector.riotx.features.home.room.list
|
package im.vector.riotx.features.home.room.list
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.riotx.core.platform.VectorViewEvents
|
import im.vector.riotx.core.platform.VectorViewEvents
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,5 +27,5 @@ sealed class RoomListViewEvents : VectorViewEvents {
|
|||||||
data class Loading(val message: CharSequence? = null) : RoomListViewEvents()
|
data class Loading(val message: CharSequence? = null) : RoomListViewEvents()
|
||||||
data class Failure(val throwable: Throwable) : RoomListViewEvents()
|
data class Failure(val throwable: Throwable) : RoomListViewEvents()
|
||||||
|
|
||||||
data class SelectRoom(val roomId: String) : RoomListViewEvents()
|
data class SelectRoom(val roomSummary: RoomSummary) : RoomListViewEvents()
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,9 @@ import im.vector.matrix.android.api.session.Session
|
|||||||
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.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
|
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.core.utils.DataSource
|
import im.vector.riotx.core.utils.DataSource
|
||||||
import im.vector.riotx.features.home.RoomListDisplayMode
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -67,13 +67,13 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
|
|||||||
is RoomListAction.MarkAllRoomsRead -> handleMarkAllRoomsRead()
|
is RoomListAction.MarkAllRoomsRead -> handleMarkAllRoomsRead()
|
||||||
is RoomListAction.LeaveRoom -> handleLeaveRoom(action)
|
is RoomListAction.LeaveRoom -> handleLeaveRoom(action)
|
||||||
is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
|
is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
|
||||||
}
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
private fun handleSelectRoom(action: RoomListAction.SelectRoom) {
|
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
|
||||||
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary.roomId))
|
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleToggleCategory(action: RoomListAction.ToggleCategory) = setState {
|
private fun handleToggleCategory(action: RoomListAction.ToggleCategory) = setState {
|
||||||
@ -204,54 +204,35 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
|
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
|
||||||
if (displayMode == RoomListDisplayMode.SHARE) {
|
// Set up init size on directChats and groupRooms as they are the biggest ones
|
||||||
val recentRooms = ArrayList<RoomSummary>(20)
|
val invites = ArrayList<RoomSummary>()
|
||||||
val otherRooms = ArrayList<RoomSummary>(rooms.size)
|
val favourites = ArrayList<RoomSummary>()
|
||||||
|
val directChats = ArrayList<RoomSummary>(rooms.size)
|
||||||
|
val groupRooms = ArrayList<RoomSummary>(rooms.size)
|
||||||
|
val lowPriorities = ArrayList<RoomSummary>()
|
||||||
|
val serverNotices = ArrayList<RoomSummary>()
|
||||||
|
|
||||||
rooms
|
rooms
|
||||||
.filter { roomListDisplayModeFilter.test(it) }
|
.filter { roomListDisplayModeFilter.test(it) }
|
||||||
.forEach { room ->
|
.forEach { room ->
|
||||||
when (room.breadcrumbsIndex) {
|
val tags = room.tags.map { it.name }
|
||||||
RoomSummary.NOT_IN_BREADCRUMBS -> otherRooms.add(room)
|
when {
|
||||||
else -> recentRooms.add(room)
|
room.membership == Membership.INVITE -> invites.add(room)
|
||||||
}
|
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
|
||||||
|
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
|
||||||
|
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
|
||||||
|
room.isDirect -> directChats.add(room)
|
||||||
|
else -> groupRooms.add(room)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return RoomSummaries().apply {
|
return RoomSummaries().apply {
|
||||||
put(RoomCategory.RECENT_ROOMS, recentRooms)
|
put(RoomCategory.INVITE, invites)
|
||||||
put(RoomCategory.OTHER_ROOMS, otherRooms)
|
put(RoomCategory.FAVOURITE, favourites)
|
||||||
}
|
put(RoomCategory.DIRECT, directChats)
|
||||||
} else {
|
put(RoomCategory.GROUP, groupRooms)
|
||||||
// Set up init size on directChats and groupRooms as they are the biggest ones
|
put(RoomCategory.LOW_PRIORITY, lowPriorities)
|
||||||
val invites = ArrayList<RoomSummary>()
|
put(RoomCategory.SERVER_NOTICE, serverNotices)
|
||||||
val favourites = ArrayList<RoomSummary>()
|
|
||||||
val directChats = ArrayList<RoomSummary>(rooms.size)
|
|
||||||
val groupRooms = ArrayList<RoomSummary>(rooms.size)
|
|
||||||
val lowPriorities = ArrayList<RoomSummary>()
|
|
||||||
val serverNotices = ArrayList<RoomSummary>()
|
|
||||||
|
|
||||||
rooms
|
|
||||||
.filter { roomListDisplayModeFilter.test(it) }
|
|
||||||
.forEach { room ->
|
|
||||||
val tags = room.tags.map { it.name }
|
|
||||||
when {
|
|
||||||
room.membership == Membership.INVITE -> invites.add(room)
|
|
||||||
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
|
|
||||||
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
|
|
||||||
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
|
|
||||||
room.isDirect -> directChats.add(room)
|
|
||||||
else -> groupRooms.add(room)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return RoomSummaries().apply {
|
|
||||||
put(RoomCategory.INVITE, invites)
|
|
||||||
put(RoomCategory.FAVOURITE, favourites)
|
|
||||||
put(RoomCategory.DIRECT, directChats)
|
|
||||||
put(RoomCategory.GROUP, groupRooms)
|
|
||||||
put(RoomCategory.LOW_PRIORITY, lowPriorities)
|
|
||||||
put(RoomCategory.SERVER_NOTICE, serverNotices)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,21 +18,18 @@ package im.vector.riotx.features.home.room.list
|
|||||||
|
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.riotx.features.home.HomeRoomListDataSource
|
import im.vector.riotx.features.home.HomeRoomListDataSource
|
||||||
import im.vector.riotx.features.home.RoomListDisplayMode
|
|
||||||
import im.vector.riotx.features.share.ShareRoomListDataSource
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
|
|
||||||
class RoomListViewModelFactory @Inject constructor(private val session: Provider<Session>,
|
class RoomListViewModelFactory @Inject constructor(private val session: Provider<Session>,
|
||||||
private val homeRoomListDataSource: Provider<HomeRoomListDataSource>,
|
private val homeRoomListDataSource: Provider<HomeRoomListDataSource>)
|
||||||
private val shareRoomListDataSource: Provider<ShareRoomListDataSource>)
|
|
||||||
: RoomListViewModel.Factory {
|
: RoomListViewModel.Factory {
|
||||||
|
|
||||||
override fun create(initialState: RoomListViewState): RoomListViewModel {
|
override fun create(initialState: RoomListViewState): RoomListViewModel {
|
||||||
return RoomListViewModel(
|
return RoomListViewModel(
|
||||||
initialState,
|
initialState,
|
||||||
session.get(),
|
session.get(),
|
||||||
if (initialState.displayMode == RoomListDisplayMode.SHARE) shareRoomListDataSource.get() else homeRoomListDataSource.get()
|
homeRoomListDataSource.get()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,10 +43,7 @@ data class RoomListViewState(
|
|||||||
val isDirectRoomsExpanded: Boolean = true,
|
val isDirectRoomsExpanded: Boolean = true,
|
||||||
val isGroupRoomsExpanded: Boolean = true,
|
val isGroupRoomsExpanded: Boolean = true,
|
||||||
val isLowPriorityRoomsExpanded: Boolean = true,
|
val isLowPriorityRoomsExpanded: Boolean = true,
|
||||||
val isServerNoticeRoomsExpanded: Boolean = true,
|
val isServerNoticeRoomsExpanded: Boolean = true
|
||||||
// For sharing
|
|
||||||
val isRecentExpanded: Boolean = true,
|
|
||||||
val isOtherExpanded: Boolean = true
|
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
|
||||||
constructor(args: RoomListParams) : this(displayMode = args.displayMode)
|
constructor(args: RoomListParams) : this(displayMode = args.displayMode)
|
||||||
@ -59,8 +56,6 @@ data class RoomListViewState(
|
|||||||
RoomCategory.GROUP -> isGroupRoomsExpanded
|
RoomCategory.GROUP -> isGroupRoomsExpanded
|
||||||
RoomCategory.LOW_PRIORITY -> isLowPriorityRoomsExpanded
|
RoomCategory.LOW_PRIORITY -> isLowPriorityRoomsExpanded
|
||||||
RoomCategory.SERVER_NOTICE -> isServerNoticeRoomsExpanded
|
RoomCategory.SERVER_NOTICE -> isServerNoticeRoomsExpanded
|
||||||
RoomCategory.RECENT_ROOMS -> isRecentExpanded
|
|
||||||
RoomCategory.OTHER_ROOMS -> isOtherExpanded
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,8 +67,6 @@ data class RoomListViewState(
|
|||||||
RoomCategory.GROUP -> copy(isGroupRoomsExpanded = !isGroupRoomsExpanded)
|
RoomCategory.GROUP -> copy(isGroupRoomsExpanded = !isGroupRoomsExpanded)
|
||||||
RoomCategory.LOW_PRIORITY -> copy(isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded)
|
RoomCategory.LOW_PRIORITY -> copy(isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded)
|
||||||
RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded)
|
RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded)
|
||||||
RoomCategory.RECENT_ROOMS -> copy(isRecentExpanded = !isRecentExpanded)
|
|
||||||
RoomCategory.OTHER_ROOMS -> copy(isOtherExpanded = !isOtherExpanded)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,11 +86,7 @@ enum class RoomCategory(@StringRes val titleRes: Int) {
|
|||||||
DIRECT(R.string.bottom_action_people_x),
|
DIRECT(R.string.bottom_action_people_x),
|
||||||
GROUP(R.string.bottom_action_rooms),
|
GROUP(R.string.bottom_action_rooms),
|
||||||
LOW_PRIORITY(R.string.low_priority_header),
|
LOW_PRIORITY(R.string.low_priority_header),
|
||||||
SERVER_NOTICE(R.string.system_alerts_header),
|
SERVER_NOTICE(R.string.system_alerts_header)
|
||||||
|
|
||||||
// For Sharing
|
|
||||||
RECENT_ROOMS(R.string.room_list_sharing_header_recent_rooms),
|
|
||||||
OTHER_ROOMS(R.string.room_list_sharing_header_other_rooms)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RoomSummaries?.isNullOrEmpty(): Boolean {
|
fun RoomSummaries?.isNullOrEmpty(): Boolean {
|
||||||
|
@ -22,7 +22,6 @@ import im.vector.matrix.android.api.session.room.model.Membership
|
|||||||
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
|
||||||
import im.vector.riotx.core.epoxy.helpFooterItem
|
import im.vector.riotx.core.epoxy.helpFooterItem
|
||||||
import im.vector.riotx.core.epoxy.noResultItem
|
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import im.vector.riotx.core.resources.UserPreferencesProvider
|
import im.vector.riotx.core.resources.UserPreferencesProvider
|
||||||
import im.vector.riotx.features.home.RoomListDisplayMode
|
import im.vector.riotx.features.home.RoomListDisplayMode
|
||||||
@ -60,7 +59,6 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
|
|||||||
val nonNullViewState = viewState ?: return
|
val nonNullViewState = viewState ?: return
|
||||||
when (nonNullViewState.displayMode) {
|
when (nonNullViewState.displayMode) {
|
||||||
RoomListDisplayMode.FILTERED -> buildFilteredRooms(nonNullViewState)
|
RoomListDisplayMode.FILTERED -> buildFilteredRooms(nonNullViewState)
|
||||||
RoomListDisplayMode.SHARE -> buildShareRooms(nonNullViewState)
|
|
||||||
else -> buildRooms(nonNullViewState)
|
else -> buildRooms(nonNullViewState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,44 +75,12 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
|
|||||||
viewState.joiningRoomsIds,
|
viewState.joiningRoomsIds,
|
||||||
viewState.joiningErrorRoomsIds,
|
viewState.joiningErrorRoomsIds,
|
||||||
viewState.rejectingRoomsIds,
|
viewState.rejectingRoomsIds,
|
||||||
viewState.rejectingErrorRoomsIds)
|
viewState.rejectingErrorRoomsIds,
|
||||||
|
emptySet())
|
||||||
|
|
||||||
addFilterFooter(viewState)
|
addFilterFooter(viewState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildShareRooms(viewState: RoomListViewState) {
|
|
||||||
var hasResult = false
|
|
||||||
val roomSummaries = viewState.asyncFilteredRooms()
|
|
||||||
|
|
||||||
roomListNameFilter.filter = viewState.roomFilter
|
|
||||||
|
|
||||||
roomSummaries?.forEach { (category, summaries) ->
|
|
||||||
val filteredSummaries = summaries
|
|
||||||
.filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
|
|
||||||
|
|
||||||
if (filteredSummaries.isEmpty()) {
|
|
||||||
return@forEach
|
|
||||||
} else {
|
|
||||||
hasResult = true
|
|
||||||
val isExpanded = viewState.isCategoryExpanded(category)
|
|
||||||
buildRoomCategory(viewState, emptyList(), category.titleRes, viewState.isCategoryExpanded(category)) {
|
|
||||||
listener?.onToggleRoomCategory(category)
|
|
||||||
}
|
|
||||||
if (isExpanded) {
|
|
||||||
buildRoomModels(filteredSummaries,
|
|
||||||
emptySet(),
|
|
||||||
emptySet(),
|
|
||||||
emptySet(),
|
|
||||||
emptySet()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hasResult) {
|
|
||||||
addNoResultItem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildRooms(viewState: RoomListViewState) {
|
private fun buildRooms(viewState: RoomListViewState) {
|
||||||
var showHelp = false
|
var showHelp = false
|
||||||
val roomSummaries = viewState.asyncFilteredRooms()
|
val roomSummaries = viewState.asyncFilteredRooms()
|
||||||
@ -131,7 +97,8 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
|
|||||||
viewState.joiningRoomsIds,
|
viewState.joiningRoomsIds,
|
||||||
viewState.joiningErrorRoomsIds,
|
viewState.joiningErrorRoomsIds,
|
||||||
viewState.rejectingRoomsIds,
|
viewState.rejectingRoomsIds,
|
||||||
viewState.rejectingErrorRoomsIds)
|
viewState.rejectingErrorRoomsIds,
|
||||||
|
emptySet())
|
||||||
// Never set showHelp to true for invitation
|
// Never set showHelp to true for invitation
|
||||||
if (category != RoomCategory.INVITE) {
|
if (category != RoomCategory.INVITE) {
|
||||||
showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp()
|
showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp()
|
||||||
@ -160,13 +127,6 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addNoResultItem() {
|
|
||||||
noResultItem {
|
|
||||||
id("no_result")
|
|
||||||
text(stringProvider.getString(R.string.no_result_placeholder))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildRoomCategory(viewState: RoomListViewState,
|
private fun buildRoomCategory(viewState: RoomListViewState,
|
||||||
summaries: List<RoomSummary>,
|
summaries: List<RoomSummary>,
|
||||||
@StringRes titleRes: Int,
|
@StringRes titleRes: Int,
|
||||||
@ -196,10 +156,17 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
|
|||||||
joiningRoomsIds: Set<String>,
|
joiningRoomsIds: Set<String>,
|
||||||
joiningErrorRoomsIds: Set<String>,
|
joiningErrorRoomsIds: Set<String>,
|
||||||
rejectingRoomsIds: Set<String>,
|
rejectingRoomsIds: Set<String>,
|
||||||
rejectingErrorRoomsIds: Set<String>) {
|
rejectingErrorRoomsIds: Set<String>,
|
||||||
|
selectedRoomIds: Set<String>) {
|
||||||
summaries.forEach { roomSummary ->
|
summaries.forEach { roomSummary ->
|
||||||
roomSummaryItemFactory
|
roomSummaryItemFactory
|
||||||
.create(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
|
.create(roomSummary,
|
||||||
|
joiningRoomsIds,
|
||||||
|
joiningErrorRoomsIds,
|
||||||
|
rejectingRoomsIds,
|
||||||
|
rejectingErrorRoomsIds,
|
||||||
|
selectedRoomIds,
|
||||||
|
listener)
|
||||||
.addTo(this)
|
.addTo(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,14 +16,17 @@
|
|||||||
|
|
||||||
package im.vector.riotx.features.home.room.list
|
package im.vector.riotx.features.home.room.list
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import com.amulyakhare.textdrawable.TextDrawable
|
||||||
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
||||||
import im.vector.matrix.android.api.util.MatrixItem
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
@ -48,11 +51,15 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||||||
@EpoxyAttribute var showHighlighted: Boolean = false
|
@EpoxyAttribute var showHighlighted: Boolean = false
|
||||||
@EpoxyAttribute var itemLongClickListener: View.OnLongClickListener? = null
|
@EpoxyAttribute var itemLongClickListener: View.OnLongClickListener? = null
|
||||||
@EpoxyAttribute var itemClickListener: View.OnClickListener? = null
|
@EpoxyAttribute var itemClickListener: View.OnClickListener? = null
|
||||||
|
@EpoxyAttribute var showSelected: Boolean = false
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
holder.rootView.setOnClickListener(itemClickListener)
|
holder.rootView.setOnClickListener(itemClickListener)
|
||||||
holder.rootView.setOnLongClickListener(itemLongClickListener)
|
holder.rootView.setOnLongClickListener {
|
||||||
|
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
|
itemLongClickListener?.onLongClick(it) ?: false
|
||||||
|
}
|
||||||
holder.titleView.text = matrixItem.getBestName()
|
holder.titleView.text = matrixItem.getBestName()
|
||||||
holder.lastEventTimeView.text = lastEventTime
|
holder.lastEventTimeView.text = lastEventTime
|
||||||
holder.lastEventView.text = lastFormattedEvent
|
holder.lastEventView.text = lastFormattedEvent
|
||||||
@ -64,6 +71,19 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||||||
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
||||||
holder.roomAvatarDecorationImageView.isVisible = encryptionTrustLevel != null
|
holder.roomAvatarDecorationImageView.isVisible = encryptionTrustLevel != null
|
||||||
holder.roomAvatarDecorationImageView.setImageResource(encryptionTrustLevel.toImageRes())
|
holder.roomAvatarDecorationImageView.setImageResource(encryptionTrustLevel.toImageRes())
|
||||||
|
renderSelection(holder, showSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderSelection(holder: Holder, isSelected: Boolean) {
|
||||||
|
if (isSelected) {
|
||||||
|
holder.avatarCheckedImageView.visibility = View.VISIBLE
|
||||||
|
val backgroundColor = ContextCompat.getColor(holder.view.context, R.color.riotx_accent)
|
||||||
|
val backgroundDrawable = TextDrawable.builder().buildRound("", backgroundColor)
|
||||||
|
holder.avatarImageView.setImageDrawable(backgroundDrawable)
|
||||||
|
} else {
|
||||||
|
holder.avatarCheckedImageView.visibility = View.GONE
|
||||||
|
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Holder : VectorEpoxyHolder() {
|
class Holder : VectorEpoxyHolder() {
|
||||||
@ -74,6 +94,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||||||
val typingView by bind<TextView>(R.id.roomTypingView)
|
val typingView by bind<TextView>(R.id.roomTypingView)
|
||||||
val draftView by bind<ImageView>(R.id.roomDraftBadge)
|
val draftView by bind<ImageView>(R.id.roomDraftBadge)
|
||||||
val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView)
|
val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView)
|
||||||
|
val avatarCheckedImageView by bind<ImageView>(R.id.roomAvatarCheckedImageView)
|
||||||
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
|
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
|
||||||
val roomAvatarDecorationImageView by bind<ImageView>(R.id.roomAvatarDecorationImageView)
|
val roomAvatarDecorationImageView by bind<ImageView>(R.id.roomAvatarDecorationImageView)
|
||||||
val rootView by bind<ViewGroup>(R.id.itemRoomLayout)
|
val rootView by bind<ViewGroup>(R.id.itemRoomLayout)
|
||||||
|
@ -25,7 +25,6 @@ import im.vector.riotx.R
|
|||||||
import im.vector.riotx.core.date.VectorDateFormatter
|
import im.vector.riotx.core.date.VectorDateFormatter
|
||||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
import im.vector.riotx.core.extensions.localDateTime
|
import im.vector.riotx.core.extensions.localDateTime
|
||||||
import im.vector.riotx.core.resources.ColorProvider
|
|
||||||
import im.vector.riotx.core.resources.DateProvider
|
import im.vector.riotx.core.resources.DateProvider
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||||
@ -36,7 +35,6 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
class RoomSummaryItemFactory @Inject constructor(private val displayableEventFormatter: DisplayableEventFormatter,
|
class RoomSummaryItemFactory @Inject constructor(private val displayableEventFormatter: DisplayableEventFormatter,
|
||||||
private val dateFormatter: VectorDateFormatter,
|
private val dateFormatter: VectorDateFormatter,
|
||||||
private val colorProvider: ColorProvider,
|
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val typingHelper: TypingHelper,
|
private val typingHelper: TypingHelper,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
@ -47,19 +45,20 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
|||||||
joiningErrorRoomsIds: Set<String>,
|
joiningErrorRoomsIds: Set<String>,
|
||||||
rejectingRoomsIds: Set<String>,
|
rejectingRoomsIds: Set<String>,
|
||||||
rejectingErrorRoomsIds: Set<String>,
|
rejectingErrorRoomsIds: Set<String>,
|
||||||
|
selectedRoomIds: Set<String>,
|
||||||
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
|
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
|
||||||
return when (roomSummary.membership) {
|
return when (roomSummary.membership) {
|
||||||
Membership.INVITE -> createInvitationItem(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
|
Membership.INVITE -> createInvitationItem(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
|
||||||
else -> createRoomItem(roomSummary, listener)
|
else -> createRoomItem(roomSummary, selectedRoomIds, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createInvitationItem(roomSummary: RoomSummary,
|
fun createInvitationItem(roomSummary: RoomSummary,
|
||||||
joiningRoomsIds: Set<String>,
|
joiningRoomsIds: Set<String>,
|
||||||
joiningErrorRoomsIds: Set<String>,
|
joiningErrorRoomsIds: Set<String>,
|
||||||
rejectingRoomsIds: Set<String>,
|
rejectingRoomsIds: Set<String>,
|
||||||
rejectingErrorRoomsIds: Set<String>,
|
rejectingErrorRoomsIds: Set<String>,
|
||||||
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
|
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
|
||||||
val secondLine = if (roomSummary.isDirect) {
|
val secondLine = if (roomSummary.isDirect) {
|
||||||
roomSummary.latestPreviewableEvent?.root?.senderId
|
roomSummary.latestPreviewableEvent?.root?.senderId
|
||||||
} else {
|
} else {
|
||||||
@ -82,10 +81,15 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
|||||||
.listener { listener?.onRoomClicked(roomSummary) }
|
.listener { listener?.onRoomClicked(roomSummary) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRoomItem(roomSummary: RoomSummary, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
|
fun createRoomItem(
|
||||||
|
roomSummary: RoomSummary,
|
||||||
|
selectedRoomIds: Set<String>,
|
||||||
|
onClick: ((RoomSummary) -> Unit)?,
|
||||||
|
onLongClick: ((RoomSummary) -> Boolean)?
|
||||||
|
): VectorEpoxyModel<*> {
|
||||||
val unreadCount = roomSummary.notificationCount
|
val unreadCount = roomSummary.notificationCount
|
||||||
val showHighlighted = roomSummary.highlightCount > 0
|
val showHighlighted = roomSummary.highlightCount > 0
|
||||||
|
val showSelected = selectedRoomIds.contains(roomSummary.roomId)
|
||||||
var latestFormattedEvent: CharSequence = ""
|
var latestFormattedEvent: CharSequence = ""
|
||||||
var latestEventTime: CharSequence = ""
|
var latestEventTime: CharSequence = ""
|
||||||
val latestEvent = roomSummary.latestPreviewableEvent
|
val latestEvent = roomSummary.latestPreviewableEvent
|
||||||
@ -119,15 +123,16 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
|||||||
.typingString(typingString)
|
.typingString(typingString)
|
||||||
.lastFormattedEvent(latestFormattedEvent)
|
.lastFormattedEvent(latestFormattedEvent)
|
||||||
.showHighlighted(showHighlighted)
|
.showHighlighted(showHighlighted)
|
||||||
|
.showSelected(showSelected)
|
||||||
.unreadNotificationCount(unreadCount)
|
.unreadNotificationCount(unreadCount)
|
||||||
.hasUnreadMessage(roomSummary.hasUnreadMessages)
|
.hasUnreadMessage(roomSummary.hasUnreadMessages)
|
||||||
.hasDraft(roomSummary.userDrafts.isNotEmpty())
|
.hasDraft(roomSummary.userDrafts.isNotEmpty())
|
||||||
.itemLongClickListener { _ ->
|
.itemLongClickListener { _ ->
|
||||||
listener?.onRoomLongClicked(roomSummary) ?: false
|
onLongClick?.invoke(roomSummary) ?: false
|
||||||
}
|
}
|
||||||
.itemClickListener(
|
.itemClickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { _ ->
|
DebouncedClickListener(View.OnClickListener { _ ->
|
||||||
listener?.onRoomClicked(roomSummary)
|
onClick?.invoke(roomSummary)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -64,15 +64,15 @@ class DefaultNavigator @Inject constructor(
|
|||||||
startActivity(context, intent, buildTask)
|
startActivity(context, intent, buildTask)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun performDeviceVerification(context: Context, otherUserId: String, sasTransationId: String) {
|
override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) {
|
||||||
val session = sessionHolder.getSafeActiveSession() ?: return
|
val session = sessionHolder.getSafeActiveSession() ?: return
|
||||||
val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransationId) ?: return
|
val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) ?: return
|
||||||
(tx as? IncomingSasVerificationTransaction)?.performAccept()
|
(tx as? IncomingSasVerificationTransaction)?.performAccept()
|
||||||
if (context is VectorBaseActivity) {
|
if (context is VectorBaseActivity) {
|
||||||
VerificationBottomSheet.withArgs(
|
VerificationBottomSheet.withArgs(
|
||||||
roomId = null,
|
roomId = null,
|
||||||
otherUserId = otherUserId,
|
otherUserId = otherUserId,
|
||||||
transactionId = sasTransationId
|
transactionId = sasTransactionId
|
||||||
).show(context.supportFragmentManager, "REQPOP")
|
).show(context.supportFragmentManager, "REQPOP")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,7 +126,7 @@ class DefaultNavigator @Inject constructor(
|
|||||||
startActivity(context, intent, buildTask)
|
startActivity(context, intent, buildTask)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData) {
|
override fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData) {
|
||||||
val args = RoomDetailArgs(roomId, null, sharedData)
|
val args = RoomDetailArgs(roomId, null, sharedData)
|
||||||
val intent = RoomDetailActivity.newIntent(activity, args)
|
val intent = RoomDetailActivity.newIntent(activity, args)
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
|
@ -26,11 +26,13 @@ interface Navigator {
|
|||||||
|
|
||||||
fun openRoom(context: Context, roomId: String, eventId: String? = null, buildTask: Boolean = false)
|
fun openRoom(context: Context, roomId: String, eventId: String? = null, buildTask: Boolean = false)
|
||||||
|
|
||||||
fun performDeviceVerification(context: Context, otherUserId: String, sasTransationId: String)
|
fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String)
|
||||||
|
|
||||||
fun requestSessionVerification(context: Context)
|
fun requestSessionVerification(context: Context)
|
||||||
|
|
||||||
fun waitSessionVerification(context: Context)
|
fun waitSessionVerification(context: Context)
|
||||||
|
|
||||||
fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData)
|
fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData)
|
||||||
|
|
||||||
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)
|
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)
|
||||||
|
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.share
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
sealed class IncomingShareAction : VectorViewModelAction {
|
||||||
|
data class SelectRoom(val roomSummary: RoomSummary, val enableMultiSelect: Boolean) : IncomingShareAction()
|
||||||
|
object ShareToSelectedRooms : IncomingShareAction()
|
||||||
|
data class ShareMedia(val keepOriginalSize: Boolean) : IncomingShareAction()
|
||||||
|
data class FilterWith(val filter: String) : IncomingShareAction()
|
||||||
|
data class UpdateSharedData(val sharedData: SharedData) : IncomingShareAction()
|
||||||
|
}
|
@ -9,126 +9,30 @@
|
|||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.V
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.share
|
package im.vector.riotx.features.share
|
||||||
|
|
||||||
import android.content.ClipDescription
|
import androidx.appcompat.widget.Toolbar
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import com.airbnb.mvrx.viewModel
|
|
||||||
import com.kbeanie.multipicker.utils.IntentUtils
|
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.extensions.addFragment
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.platform.ToolbarConfigurable
|
||||||
import im.vector.riotx.core.extensions.replaceFragment
|
|
||||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
import im.vector.riotx.features.attachments.AttachmentsHelper
|
|
||||||
import im.vector.riotx.features.home.LoadingFragment
|
|
||||||
import im.vector.riotx.features.home.RoomListDisplayMode
|
|
||||||
import im.vector.riotx.features.home.room.list.RoomListFragment
|
|
||||||
import im.vector.riotx.features.home.room.list.RoomListParams
|
|
||||||
import im.vector.riotx.features.login.LoginActivity
|
|
||||||
import kotlinx.android.synthetic.main.activity_incoming_share.*
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class IncomingShareActivity :
|
class IncomingShareActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
VectorBaseActivity(), AttachmentsHelper.Callback {
|
|
||||||
|
|
||||||
@Inject lateinit var sessionHolder: ActiveSessionHolder
|
override fun getLayoutRes() = R.layout.activity_simple
|
||||||
@Inject lateinit var incomingShareViewModelFactory: IncomingShareViewModel.Factory
|
|
||||||
private lateinit var attachmentsHelper: AttachmentsHelper
|
|
||||||
// Do not remove, even if not used, it instantiates the view model
|
|
||||||
@Suppress("unused")
|
|
||||||
private val viewModel: IncomingShareViewModel by viewModel()
|
|
||||||
private val roomListFragment: RoomListFragment?
|
|
||||||
get() {
|
|
||||||
return supportFragmentManager.findFragmentById(R.id.shareRoomListFragmentContainer) as? RoomListFragment
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLayoutRes() = R.layout.activity_incoming_share
|
override fun initUiAndData() {
|
||||||
|
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
|
||||||
injector.inject(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
// If we are not logged in, stop the sharing process and open login screen.
|
|
||||||
// In the future, we might want to relaunch the sharing process after login.
|
|
||||||
if (!sessionHolder.hasActiveSession()) {
|
|
||||||
startLoginActivity()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
configureToolbar(incomingShareToolbar)
|
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
replaceFragment(R.id.shareRoomListFragmentContainer, LoadingFragment::class.java)
|
addFragment(R.id.simpleFragmentContainer, IncomingShareFragment::class.java)
|
||||||
}
|
}
|
||||||
attachmentsHelper = AttachmentsHelper.create(this, this).register()
|
|
||||||
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
|
|
||||||
var isShareManaged = attachmentsHelper.handleShareIntent(
|
|
||||||
IntentUtils.getPickerIntentForSharing(intent)
|
|
||||||
)
|
|
||||||
if (!isShareManaged) {
|
|
||||||
isShareManaged = handleTextShare(intent)
|
|
||||||
}
|
|
||||||
if (!isShareManaged) {
|
|
||||||
cannotManageShare()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cannotManageShare()
|
|
||||||
}
|
|
||||||
|
|
||||||
incomingShareSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
|
||||||
override fun onQueryTextSubmit(query: String): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String): Boolean {
|
|
||||||
roomListFragment?.filterRoomsWith(newText)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
|
override fun configure(toolbar: Toolbar) {
|
||||||
val roomListParams = RoomListParams(RoomListDisplayMode.SHARE, sharedData = SharedData.Attachments(attachments))
|
configureToolbar(toolbar, displayBack = false)
|
||||||
replaceFragment(R.id.shareRoomListFragmentContainer, RoomListFragment::class.java, roomListParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachmentsProcessFailed() {
|
|
||||||
cannotManageShare()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cannotManageShare() {
|
|
||||||
Toast.makeText(this, R.string.error_handling_incoming_share, Toast.LENGTH_LONG).show()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleTextShare(intent: Intent): Boolean {
|
|
||||||
if (intent.type == ClipDescription.MIMETYPE_TEXT_PLAIN) {
|
|
||||||
val sharedText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
|
|
||||||
return if (sharedText.isNullOrEmpty()) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
val roomListParams = RoomListParams(RoomListDisplayMode.SHARE, sharedData = SharedData.Text(sharedText))
|
|
||||||
replaceFragment(R.id.shareRoomListFragmentContainer, RoomListFragment::class.java, roomListParams)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startLoginActivity() {
|
|
||||||
val intent = LoginActivity.newIntent(this, null)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.share
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import com.airbnb.mvrx.Incomplete
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.epoxy.loadingItem
|
||||||
|
import im.vector.riotx.core.epoxy.noResultItem
|
||||||
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import im.vector.riotx.features.home.room.list.RoomSummaryItemFactory
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class IncomingShareController @Inject constructor(private val roomSummaryItemFactory: RoomSummaryItemFactory,
|
||||||
|
private val stringProvider: StringProvider) : TypedEpoxyController<IncomingShareViewState>() {
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onRoomClicked(roomSummary: RoomSummary)
|
||||||
|
fun onRoomLongClicked(roomSummary: RoomSummary): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
override fun buildModels(data: IncomingShareViewState) {
|
||||||
|
if (data.sharedData == null || data.filteredRoomSummaries is Incomplete) {
|
||||||
|
loadingItem {
|
||||||
|
id("loading")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val roomSummaries = data.filteredRoomSummaries()
|
||||||
|
if (roomSummaries.isNullOrEmpty()) {
|
||||||
|
noResultItem {
|
||||||
|
id("no_result")
|
||||||
|
text(stringProvider.getString(R.string.no_result_placeholder))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
roomSummaries.forEach { roomSummary ->
|
||||||
|
roomSummaryItemFactory
|
||||||
|
.createRoomItem(roomSummary, data.selectedRoomIds, callback?.let { it::onRoomClicked }, callback?.let { it::onRoomLongClicked })
|
||||||
|
.addTo(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,241 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.share
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ClipDescription
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
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.room.model.RoomSummary
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.riotx.core.extensions.cleanup
|
||||||
|
import im.vector.riotx.core.extensions.configureWith
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
|
||||||
|
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_PICK_ATTACHMENT
|
||||||
|
import im.vector.riotx.core.utils.allGranted
|
||||||
|
import im.vector.riotx.core.utils.checkPermissions
|
||||||
|
import im.vector.riotx.features.attachments.AttachmentsHelper
|
||||||
|
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity
|
||||||
|
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs
|
||||||
|
import im.vector.riotx.features.login.LoginActivity
|
||||||
|
import kotlinx.android.synthetic.main.fragment_incoming_share.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the list of rooms
|
||||||
|
* The user can select multiple rooms to send the data to
|
||||||
|
*/
|
||||||
|
class IncomingShareFragment @Inject constructor(
|
||||||
|
val incomingShareViewModelFactory: IncomingShareViewModel.Factory,
|
||||||
|
private val incomingShareController: IncomingShareController,
|
||||||
|
private val sessionHolder: ActiveSessionHolder
|
||||||
|
) : VectorBaseFragment(), AttachmentsHelper.Callback, IncomingShareController.Callback {
|
||||||
|
|
||||||
|
private lateinit var attachmentsHelper: AttachmentsHelper
|
||||||
|
private val viewModel: IncomingShareViewModel by fragmentViewModel()
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.fragment_incoming_share
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
// If we are not logged in, stop the sharing process and open login screen.
|
||||||
|
// In the future, we might want to relaunch the sharing process after login.
|
||||||
|
if (!sessionHolder.hasActiveSession()) {
|
||||||
|
startLoginActivity()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
setupRecyclerView()
|
||||||
|
setupToolbar(incomingShareToolbar)
|
||||||
|
attachmentsHelper = AttachmentsHelper.create(this, this).register()
|
||||||
|
|
||||||
|
val intent = vectorBaseActivity.intent
|
||||||
|
val isShareManaged = when (intent?.action) {
|
||||||
|
Intent.ACTION_SEND -> {
|
||||||
|
var isShareManaged = attachmentsHelper.handleShareIntent(IntentUtils.getPickerIntentForSharing(intent))
|
||||||
|
if (!isShareManaged) {
|
||||||
|
isShareManaged = handleTextShare(intent)
|
||||||
|
}
|
||||||
|
isShareManaged
|
||||||
|
}
|
||||||
|
Intent.ACTION_SEND_MULTIPLE -> attachmentsHelper.handleShareIntent(intent)
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isShareManaged) {
|
||||||
|
cannotManageShare(R.string.error_handling_incoming_share)
|
||||||
|
}
|
||||||
|
|
||||||
|
incomingShareSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String): Boolean {
|
||||||
|
viewModel.handle(IncomingShareAction.FilterWith(newText))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
sendShareButton.setOnClickListener { _ ->
|
||||||
|
handleSendShare()
|
||||||
|
}
|
||||||
|
viewModel.observeViewEvents {
|
||||||
|
when (it) {
|
||||||
|
is IncomingShareViewEvents.ShareToRoom -> handleShareToRoom(it)
|
||||||
|
is IncomingShareViewEvents.EditMediaBeforeSending -> handleEditMediaBeforeSending(it)
|
||||||
|
is IncomingShareViewEvents.MultipleRoomsShareDone -> handleMultipleRoomsShareDone(it)
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMultipleRoomsShareDone(viewEvent: IncomingShareViewEvents.MultipleRoomsShareDone) {
|
||||||
|
requireActivity().let {
|
||||||
|
navigator.openRoom(it, viewEvent.roomId)
|
||||||
|
it.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEditMediaBeforeSending(event: IncomingShareViewEvents.EditMediaBeforeSending) {
|
||||||
|
val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(event.contentAttachmentData))
|
||||||
|
startActivityForResult(intent, AttachmentsPreviewActivity.REQUEST_CODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (!hasBeenHandled && resultCode == Activity.RESULT_OK && data != null) {
|
||||||
|
when (requestCode) {
|
||||||
|
AttachmentsPreviewActivity.REQUEST_CODE -> {
|
||||||
|
val sendData = AttachmentsPreviewActivity.getOutput(data)
|
||||||
|
val keepOriginalSize = AttachmentsPreviewActivity.getKeepOriginalSize(data)
|
||||||
|
viewModel.handle(IncomingShareAction.UpdateSharedData(SharedData.Attachments(sendData)))
|
||||||
|
viewModel.handle(IncomingShareAction.ShareMedia(keepOriginalSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// We need the read file permission
|
||||||
|
checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
|
||||||
|
if (requestCode == PERMISSION_REQUEST_CODE_PICK_ATTACHMENT && !allGranted(grantResults)) {
|
||||||
|
// Permission is mandatory
|
||||||
|
cannotManageShare(R.string.missing_permissions_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleShareToRoom(event: IncomingShareViewEvents.ShareToRoom) {
|
||||||
|
if (event.showAlert) {
|
||||||
|
showConfirmationDialog(event.roomSummary, event.sharedData)
|
||||||
|
} else {
|
||||||
|
navigator.openRoomForSharingAndFinish(requireActivity(), event.roomSummary.roomId, event.sharedData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSendShare() {
|
||||||
|
viewModel.handle(IncomingShareAction.ShareToSelectedRooms)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
incomingShareController.callback = null
|
||||||
|
incomingShareRoomList.cleanup()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
incomingShareRoomList.configureWith(incomingShareController, hasFixedSize = true)
|
||||||
|
incomingShareController.callback = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
|
||||||
|
val sharedData = SharedData.Attachments(attachments)
|
||||||
|
viewModel.handle(IncomingShareAction.UpdateSharedData(sharedData))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachmentsProcessFailed() {
|
||||||
|
cannotManageShare(R.string.error_handling_incoming_share)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cannotManageShare(@StringRes messageResId: Int) {
|
||||||
|
Toast.makeText(requireContext(), messageResId, Toast.LENGTH_LONG).show()
|
||||||
|
requireActivity().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleTextShare(intent: Intent): Boolean {
|
||||||
|
if (intent.type == ClipDescription.MIMETYPE_TEXT_PLAIN) {
|
||||||
|
val sharedText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
|
||||||
|
return if (sharedText.isNullOrEmpty()) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
val sharedData = SharedData.Text(sharedText)
|
||||||
|
viewModel.handle(IncomingShareAction.UpdateSharedData(sharedData))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showConfirmationDialog(roomSummary: RoomSummary, sharedData: SharedData) {
|
||||||
|
AlertDialog.Builder(requireActivity())
|
||||||
|
.setTitle(R.string.send_attachment)
|
||||||
|
.setMessage(getString(R.string.share_confirm_room, roomSummary.displayName))
|
||||||
|
.setPositiveButton(R.string.send) { _, _ ->
|
||||||
|
navigator.openRoomForSharingAndFinish(requireActivity(), roomSummary.roomId, sharedData)
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startLoginActivity() {
|
||||||
|
val intent = LoginActivity.newIntent(requireActivity(), null)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
startActivity(intent)
|
||||||
|
requireActivity().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) {
|
||||||
|
sendShareButton.isVisible = it.isInMultiSelectionMode
|
||||||
|
incomingShareController.setData(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRoomClicked(roomSummary: RoomSummary) {
|
||||||
|
viewModel.handle(IncomingShareAction.SelectRoom(roomSummary, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRoomLongClicked(roomSummary: RoomSummary): Boolean {
|
||||||
|
viewModel.handle(IncomingShareAction.SelectRoom(roomSummary, true))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.share
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
|
import im.vector.riotx.core.platform.VectorViewEvents
|
||||||
|
|
||||||
|
sealed class IncomingShareViewEvents : VectorViewEvents {
|
||||||
|
data class ShareToRoom(val roomSummary: RoomSummary,
|
||||||
|
val sharedData: SharedData,
|
||||||
|
val showAlert: Boolean) : IncomingShareViewEvents()
|
||||||
|
|
||||||
|
data class EditMediaBeforeSending(val contentAttachmentData: List<ContentAttachmentData>) : IncomingShareViewEvents()
|
||||||
|
data class MultipleRoomsShareDone(val roomId: String) : IncomingShareViewEvents()
|
||||||
|
}
|
@ -16,71 +16,183 @@
|
|||||||
|
|
||||||
package im.vector.riotx.features.share
|
package im.vector.riotx.features.share
|
||||||
|
|
||||||
import com.airbnb.mvrx.ActivityViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
import com.airbnb.mvrx.MvRxState
|
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.matrix.android.api.extensions.orFalse
|
||||||
|
import im.vector.matrix.android.api.query.QueryStringValue
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
|
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotx.ActiveSessionDataSource
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.EmptyAction
|
|
||||||
import im.vector.riotx.core.platform.EmptyViewEvents
|
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
import im.vector.riotx.features.attachments.isPreviewable
|
||||||
|
import im.vector.riotx.features.attachments.toGroupedContentAttachmentData
|
||||||
import im.vector.riotx.features.home.room.list.BreadcrumbsRoomComparator
|
import im.vector.riotx.features.home.room.list.BreadcrumbsRoomComparator
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
data class IncomingShareState(private val dummy: Boolean = false) : MvRxState
|
class IncomingShareViewModel @AssistedInject constructor(
|
||||||
|
@Assisted initialState: IncomingShareViewState,
|
||||||
/**
|
private val session: Session,
|
||||||
* View model used to observe the room list and post update to the ShareRoomListObservableStore
|
private val breadcrumbsRoomComparator: BreadcrumbsRoomComparator)
|
||||||
*/
|
: VectorViewModel<IncomingShareViewState, IncomingShareAction, IncomingShareViewEvents>(initialState) {
|
||||||
class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: IncomingShareState,
|
|
||||||
private val sessionObservableStore: ActiveSessionDataSource,
|
|
||||||
private val shareRoomListObservableStore: ShareRoomListDataSource,
|
|
||||||
private val breadcrumbsRoomComparator: BreadcrumbsRoomComparator)
|
|
||||||
: VectorViewModel<IncomingShareState, EmptyAction, EmptyViewEvents>(initialState) {
|
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: IncomingShareState): IncomingShareViewModel
|
fun create(initialState: IncomingShareViewState): IncomingShareViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<IncomingShareViewModel, IncomingShareState> {
|
companion object : MvRxViewModelFactory<IncomingShareViewModel, IncomingShareViewState> {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
override fun create(viewModelContext: ViewModelContext, state: IncomingShareState): IncomingShareViewModel? {
|
override fun create(viewModelContext: ViewModelContext, state: IncomingShareViewState): IncomingShareViewModel? {
|
||||||
val activity: IncomingShareActivity = (viewModelContext as ActivityViewModelContext).activity()
|
val fragment: IncomingShareFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
return activity.incomingShareViewModelFactory.create(state)
|
return fragment.incomingShareViewModelFactory.create(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val filterStream: BehaviorRelay<String> = BehaviorRelay.createDefault("")
|
||||||
|
|
||||||
init {
|
init {
|
||||||
observeRoomSummaries()
|
observeRoomSummaries()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeRoomSummaries() {
|
private fun observeRoomSummaries() {
|
||||||
val queryParams = roomSummaryQueryParams()
|
val queryParams = roomSummaryQueryParams {
|
||||||
sessionObservableStore.observe()
|
memberships = listOf(Membership.JOIN)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
}
|
||||||
.switchMap {
|
session
|
||||||
it.orNull()?.rx()?.liveRoomSummaries(queryParams)
|
.rx().liveRoomSummaries(queryParams)
|
||||||
?: Observable.just(emptyList())
|
.execute {
|
||||||
|
copy(roomSummaries = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
filterStream
|
||||||
|
.switchMap { filter ->
|
||||||
|
val displayNameQuery = if (filter.isEmpty()) {
|
||||||
|
QueryStringValue.NoCondition
|
||||||
|
} else {
|
||||||
|
QueryStringValue.Contains(filter, QueryStringValue.Case.INSENSITIVE)
|
||||||
|
}
|
||||||
|
val filterQueryParams = roomSummaryQueryParams {
|
||||||
|
displayName = displayNameQuery
|
||||||
|
memberships = listOf(Membership.JOIN)
|
||||||
|
}
|
||||||
|
session.rx().liveRoomSummaries(filterQueryParams)
|
||||||
}
|
}
|
||||||
.throttleLast(300, TimeUnit.MILLISECONDS)
|
.throttleLast(300, TimeUnit.MILLISECONDS)
|
||||||
.map {
|
.map { it.sortedWith(breadcrumbsRoomComparator) }
|
||||||
it.sortedWith(breadcrumbsRoomComparator)
|
.execute {
|
||||||
|
copy(filteredRoomSummaries = it)
|
||||||
}
|
}
|
||||||
.subscribe {
|
|
||||||
shareRoomListObservableStore.post(it)
|
|
||||||
}
|
|
||||||
.disposeOnClear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: EmptyAction) {
|
override fun handle(action: IncomingShareAction) {
|
||||||
// No op
|
when (action) {
|
||||||
|
is IncomingShareAction.SelectRoom -> handleSelectRoom(action)
|
||||||
|
is IncomingShareAction.ShareToSelectedRooms -> handleShareToSelectedRooms()
|
||||||
|
is IncomingShareAction.ShareMedia -> handleShareMediaToSelectedRooms(action)
|
||||||
|
is IncomingShareAction.FilterWith -> handleFilter(action)
|
||||||
|
is IncomingShareAction.UpdateSharedData -> handleUpdateSharedData(action)
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateSharedData(action: IncomingShareAction.UpdateSharedData) {
|
||||||
|
setState { copy(sharedData = action.sharedData) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleFilter(action: IncomingShareAction.FilterWith) {
|
||||||
|
filterStream.accept(action.filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleShareToSelectedRooms() = withState { state ->
|
||||||
|
val sharedData = state.sharedData ?: return@withState
|
||||||
|
if (state.selectedRoomIds.size == 1) {
|
||||||
|
// In this case the edition of the media will be handled by the RoomDetailFragment
|
||||||
|
val selectedRoomId = state.selectedRoomIds.first()
|
||||||
|
val selectedRoom = state.roomSummaries()?.find { it.roomId == selectedRoomId } ?: return@withState
|
||||||
|
_viewEvents.post(IncomingShareViewEvents.ShareToRoom(selectedRoom, sharedData, showAlert = false))
|
||||||
|
} else {
|
||||||
|
when (sharedData) {
|
||||||
|
is SharedData.Text -> {
|
||||||
|
state.selectedRoomIds.forEach { roomId ->
|
||||||
|
val room = session.getRoom(roomId)
|
||||||
|
room?.sendTextMessage(sharedData.text)
|
||||||
|
}
|
||||||
|
// This is it, pass the first roomId to let the screen open it
|
||||||
|
_viewEvents.post(IncomingShareViewEvents.MultipleRoomsShareDone(state.selectedRoomIds.first()))
|
||||||
|
}
|
||||||
|
is SharedData.Attachments -> {
|
||||||
|
shareAttachments(sharedData.attachmentData, state.selectedRoomIds, proposeMediaEdition = true, compressMediaBeforeSending = false)
|
||||||
|
}
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleShareMediaToSelectedRooms(action: IncomingShareAction.ShareMedia) = withState { state ->
|
||||||
|
(state.sharedData as? SharedData.Attachments)?.let {
|
||||||
|
shareAttachments(it.attachmentData, state.selectedRoomIds, proposeMediaEdition = false, compressMediaBeforeSending = !action.keepOriginalSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shareAttachments(attachmentData: List<ContentAttachmentData>,
|
||||||
|
selectedRoomIds: Set<String>,
|
||||||
|
proposeMediaEdition: Boolean,
|
||||||
|
compressMediaBeforeSending: Boolean) {
|
||||||
|
if (proposeMediaEdition) {
|
||||||
|
val grouped = attachmentData.toGroupedContentAttachmentData()
|
||||||
|
if (grouped.notPreviewables.isNotEmpty()) {
|
||||||
|
// Send the not previewable attachments right now (?)
|
||||||
|
// Pick the first room to send the media
|
||||||
|
selectedRoomIds.firstOrNull()
|
||||||
|
?.let { roomId -> session.getRoom(roomId) }
|
||||||
|
?.sendMedias(grouped.notPreviewables, compressMediaBeforeSending, selectedRoomIds)
|
||||||
|
|
||||||
|
// Ensure they will not be sent twice
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
sharedData = SharedData.Attachments(grouped.previewables)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (grouped.previewables.isNotEmpty()) {
|
||||||
|
// In case of multiple share of media, edit them first
|
||||||
|
_viewEvents.post(IncomingShareViewEvents.EditMediaBeforeSending(grouped.previewables))
|
||||||
|
} else {
|
||||||
|
// This is it, pass the first roomId to let the screen open it
|
||||||
|
_viewEvents.post(IncomingShareViewEvents.MultipleRoomsShareDone(selectedRoomIds.first()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pick the first room to send the media
|
||||||
|
selectedRoomIds.firstOrNull()
|
||||||
|
?.let { roomId -> session.getRoom(roomId) }
|
||||||
|
?.sendMedias(attachmentData, compressMediaBeforeSending, selectedRoomIds)
|
||||||
|
// This is it, pass the first roomId to let the screen open it
|
||||||
|
_viewEvents.post(IncomingShareViewEvents.MultipleRoomsShareDone(selectedRoomIds.first()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSelectRoom(action: IncomingShareAction.SelectRoom) = withState { state ->
|
||||||
|
if (state.isInMultiSelectionMode) {
|
||||||
|
val selectedRooms = state.selectedRoomIds
|
||||||
|
val newSelectedRooms = if (selectedRooms.contains(action.roomSummary.roomId)) {
|
||||||
|
selectedRooms.minus(action.roomSummary.roomId)
|
||||||
|
} else {
|
||||||
|
selectedRooms.plus(action.roomSummary.roomId)
|
||||||
|
}
|
||||||
|
setState { copy(isInMultiSelectionMode = newSelectedRooms.isNotEmpty(), selectedRoomIds = newSelectedRooms) }
|
||||||
|
} else if (action.enableMultiSelect) {
|
||||||
|
setState { copy(isInMultiSelectionMode = true, selectedRoomIds = setOf(action.roomSummary.roomId)) }
|
||||||
|
} else {
|
||||||
|
val sharedData = state.sharedData ?: return@withState
|
||||||
|
// Do not show alert if the shared data contains only previewable attachments, because the user will get another chance to cancel the share
|
||||||
|
val doNotShowAlert = (sharedData as? SharedData.Attachments)?.attachmentData?.all { it.isPreviewable() }.orFalse()
|
||||||
|
_viewEvents.post(IncomingShareViewEvents.ShareToRoom(action.roomSummary, sharedData, !doNotShowAlert))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.share
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.Async
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
|
|
||||||
|
data class IncomingShareViewState(
|
||||||
|
val sharedData: SharedData? = null,
|
||||||
|
val roomSummaries: Async<List<RoomSummary>> = Uninitialized,
|
||||||
|
val filteredRoomSummaries: Async<List<RoomSummary>> = Uninitialized,
|
||||||
|
val selectedRoomIds: Set<String> = emptySet(),
|
||||||
|
val isInMultiSelectionMode: Boolean = false
|
||||||
|
) : MvRxState
|
@ -32,4 +32,10 @@ sealed class ActivityOtherThemes(@StyleRes val dark: Int,
|
|||||||
R.style.AppTheme_Black,
|
R.style.AppTheme_Black,
|
||||||
R.style.AppTheme_Status
|
R.style.AppTheme_Status
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object AttachmentsPreview : ActivityOtherThemes(
|
||||||
|
R.style.AppTheme_AttachmentsPreview,
|
||||||
|
R.style.AppTheme_AttachmentsPreview,
|
||||||
|
R.style.AppTheme_AttachmentsPreview
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="@color/riotx_accent" android:state_checked="true" />
|
||||||
|
<item android:color="@android:color/transparent" />
|
||||||
|
</selector>
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<solid android:color="@color/checked_accent_color_selector"/>
|
||||||
|
|
||||||
|
</shape>
|
@ -13,7 +13,6 @@
|
|||||||
style="@style/VectorToolbarStyle"
|
style="@style/VectorToolbarStyle"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
android:elevation="4dp"
|
|
||||||
app:contentInsetStart="0dp"
|
app:contentInsetStart="0dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
75
vector/src/main/res/layout/fragment_attachments_preview.xml
Normal file
75
vector/src/main/res/layout/fragment_attachments_preview.xml
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/attachmentPreviewerBigList"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:listitem="@layout/item_attachment_big_preview" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/attachmentPreviewerToolbar"
|
||||||
|
style="@style/VectorToolbarStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="#40000000"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:title="Title"
|
||||||
|
tools:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/attachmentPreviewerBottomContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="#40000000"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/attachmentPreviewerMiniatureList"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/attachmentPreviewerSendImageOriginalSize"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:itemCount="1"
|
||||||
|
tools:listitem="@layout/item_attachment_miniature_preview" />
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:id="@+id/attachmentPreviewerSendImageOriginalSize"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/attachmentPreviewerMiniatureList"
|
||||||
|
tools:text="@plurals/send_images_with_original_size" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/attachmentPreviewerSendButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:src="@drawable/ic_send"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/attachmentPreviewerBottomContainer"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/attachmentPreviewerBottomContainer" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
58
vector/src/main/res/layout/fragment_incoming_share.xml
Normal file
58
vector/src/main/res/layout/fragment_incoming_share.xml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/incomingShareToolbar"
|
||||||
|
style="@style/VectorToolbarStyle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:contentInsetStart="0dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SearchView
|
||||||
|
android:id="@+id/incomingShareSearchView"
|
||||||
|
style="@style/VectorSearchView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:queryHint="@string/room_filtering_filter_hint"
|
||||||
|
app:searchIcon="@drawable/ic_filter" />
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/incomingShareRoomList"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/incomingShareToolbar"
|
||||||
|
tools:listitem="@layout/item_room" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/sendShareButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:accessibilityTraversalBefore="@id/incomingShareRoomList"
|
||||||
|
android:contentDescription="@string/a11y_create_room"
|
||||||
|
android:src="@drawable/ic_send"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
14
vector/src/main/res/layout/item_attachment_big_preview.xml
Normal file
14
vector/src/main/res/layout/item_attachment_big_preview.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/attachmentBigImageView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="2dp"
|
||||||
|
card_view:cardBackgroundColor="@android:color/transparent"
|
||||||
|
card_view:cardElevation="0dp">
|
||||||
|
|
||||||
|
<im.vector.riotx.core.platform.CheckableImageView
|
||||||
|
android:id="@+id/attachmentMiniatureImageView"
|
||||||
|
android:layout_width="64dp"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:background="@drawable/background_checked_accent_color"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
@ -21,21 +21,37 @@
|
|||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
<FrameLayout
|
||||||
android:id="@+id/roomAvatarImageView"
|
android:id="@+id/roomAvatarContainer"
|
||||||
android:layout_width="56dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="56dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
tools:src="@tools:sample/avatars" />
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/roomAvatarImageView"
|
||||||
|
android:layout_width="56dp"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/roomAvatarCheckedImageView"
|
||||||
|
android:layout_width="56dp"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
android:src="@drawable/ic_material_done"
|
||||||
|
android:tint="@android:color/white"
|
||||||
|
android:visibility="visible" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/roomAvatarDecorationImageView"
|
android:id="@+id/roomAvatarDecorationImageView"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
app:layout_constraintCircle="@+id/roomAvatarImageView"
|
app:layout_constraintCircle="@id/roomAvatarContainer"
|
||||||
app:layout_constraintCircleAngle="135"
|
app:layout_constraintCircleAngle="135"
|
||||||
app:layout_constraintCircleRadius="28dp"
|
app:layout_constraintCircleRadius="28dp"
|
||||||
tools:ignore="MissingConstraints"
|
tools:ignore="MissingConstraints"
|
||||||
@ -47,7 +63,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="12dp"
|
android:layout_height="12dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/roomAvatarImageView"
|
app:layout_constraintTop_toBottomOf="@id/roomAvatarContainer"
|
||||||
tools:layout_marginStart="20dp" />
|
tools:layout_marginStart="20dp" />
|
||||||
|
|
||||||
<im.vector.riotx.core.platform.EllipsizingTextView
|
<im.vector.riotx.core.platform.EllipsizingTextView
|
||||||
@ -69,7 +85,7 @@
|
|||||||
app:layout_constraintEnd_toStartOf="@+id/roomDraftBadge"
|
app:layout_constraintEnd_toStartOf="@+id/roomDraftBadge"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
app:layout_constraintHorizontal_chainStyle="packed"
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
app:layout_constraintStart_toEndOf="@id/roomAvatarImageView"
|
app:layout_constraintStart_toEndOf="@id/roomAvatarContainer"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="@sample/matrix.json/data/displayName" />
|
tools:text="@sample/matrix.json/data/displayName" />
|
||||||
|
|
||||||
|
19
vector/src/main/res/menu/vector_attachments_preview.xml
Executable file
19
vector/src/main/res/menu/vector_attachments_preview.xml
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:context=".features.attachments.preview.AttachmentsPreviewActivity">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/attachmentsPreviewRemoveAction"
|
||||||
|
android:icon="@drawable/ic_delete"
|
||||||
|
android:title="@string/delete"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/attachmentsPreviewEditAction"
|
||||||
|
android:icon="@drawable/ic_edit"
|
||||||
|
android:title="@string/edit"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
|
||||||
|
</menu>
|
9
vector/src/main/res/values-v21/theme_common.xml
Normal file
9
vector/src/main/res/values-v21/theme_common.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="AppTheme.AttachmentsPreview" parent="AppTheme.Base.Black">
|
||||||
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
@ -23,7 +23,14 @@
|
|||||||
|
|
||||||
<!-- BEGIN Strings added by Benoit -->
|
<!-- BEGIN Strings added by Benoit -->
|
||||||
<string name="message_action_item_redact">Remove…</string>
|
<string name="message_action_item_redact">Remove…</string>
|
||||||
|
<string name="share_confirm_room">Do you want to send this attachment to %1$s?</string>
|
||||||
|
<plurals name="send_images_with_original_size">
|
||||||
|
<item quantity="one">Send image with the original size</item>
|
||||||
|
<item quantity="other">Send images with the original size</item>
|
||||||
|
</plurals>
|
||||||
<string name="login_signup_username_hint">Username</string>
|
<string name="login_signup_username_hint">Username</string>
|
||||||
|
<!-- END Strings added by Benoit -->
|
||||||
|
|
||||||
<!-- BEGIN Strings added by Benoit -->
|
<!-- BEGIN Strings added by Benoit -->
|
||||||
|
|
||||||
<!-- END Strings added by Benoit -->
|
<!-- END Strings added by Benoit -->
|
||||||
|
@ -8,4 +8,6 @@
|
|||||||
<item name="colorPrimaryDark">@color/primary_color_dark</item>
|
<item name="colorPrimaryDark">@color/primary_color_dark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.AttachmentsPreview" parent="AppTheme.Base.Black"/>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user