mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 21:48:50 +03:00
Merge pull request #1010 from vector-im/feature/attachment_process
Attachment process
This commit is contained in:
commit
8d1b2b35fd
72 changed files with 2145 additions and 428 deletions
|
@ -3,7 +3,10 @@ Changes in RiotX 0.17.0 (2020-XX-XX)
|
|||
|
||||
Features ✨:
|
||||
- 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 🙌:
|
||||
-
|
||||
|
|
|
@ -34,6 +34,9 @@ allprojects {
|
|||
includeGroupByRegex "com\\.github\\.jaiselrahman"
|
||||
// And monarchy
|
||||
includeGroupByRegex "com\\.github\\.Zhuinden"
|
||||
// And ucrop
|
||||
includeGroupByRegex "com\\.github\\.yalantis"
|
||||
// JsonViewer
|
||||
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,6 +119,7 @@ dependencies {
|
|||
|
||||
// Image
|
||||
implementation 'androidx.exifinterface:exifinterface:1.1.0'
|
||||
implementation 'id.zelory:compressor:3.0.0'
|
||||
|
||||
// Database
|
||||
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
|
||||
|
|
|
@ -29,6 +29,7 @@ data class ContentAttachmentData(
|
|||
val width: Long? = 0,
|
||||
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||
val name: String? = null,
|
||||
val queryUri: String,
|
||||
val path: String,
|
||||
val mimeType: String?,
|
||||
val type: Type
|
||||
|
|
|
@ -51,16 +51,26 @@ interface SendService {
|
|||
/**
|
||||
* Method to send a media asynchronously.
|
||||
* @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]
|
||||
*/
|
||||
fun sendMedia(attachment: ContentAttachmentData): Cancelable
|
||||
fun sendMedia(attachment: ContentAttachmentData,
|
||||
compressBeforeSending: Boolean,
|
||||
roomIds: Set<String>): Cancelable
|
||||
|
||||
/**
|
||||
* Method to send a list of media asynchronously.
|
||||
* @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]
|
||||
*/
|
||||
fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable
|
||||
fun sendMedias(attachments: List<ContentAttachmentData>,
|
||||
compressBeforeSending: Boolean,
|
||||
roomIds: Set<String>): Cancelable
|
||||
|
||||
/**
|
||||
* Send a poll to the room.
|
||||
|
|
|
@ -17,7 +17,11 @@
|
|||
package im.vector.matrix.android.internal.di
|
||||
|
||||
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
|
||||
|
||||
internal class WorkManagerProvider @Inject constructor(
|
||||
|
@ -54,5 +58,7 @@ internal class WorkManagerProvider @Inject constructor(
|
|||
val workConstraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.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.relation.SendRelationWorker
|
||||
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.SendEventWorker
|
||||
import im.vector.matrix.android.internal.session.signout.SignOutModule
|
||||
|
@ -85,23 +86,25 @@ internal interface SessionComponent {
|
|||
|
||||
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
|
||||
interface Factory {
|
||||
|
|
|
@ -17,18 +17,25 @@
|
|||
package im.vector.matrix.android.internal.session.content
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
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.events.model.Event
|
||||
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.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.model.rest.EncryptedFileInfo
|
||||
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.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||
|
@ -38,15 +45,21 @@ import java.io.File
|
|||
import java.io.FileInputStream
|
||||
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)
|
||||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
val roomId: String,
|
||||
val event: Event,
|
||||
val events: List<Event>,
|
||||
val attachment: ContentAttachmentData,
|
||||
val isRoomEncrypted: Boolean,
|
||||
val compressBeforeSending: Boolean,
|
||||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams
|
||||
|
||||
|
@ -67,20 +80,50 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
|
||||
val eventId = params.event.eventId ?: return Result.success()
|
||||
val attachment = params.attachment
|
||||
|
||||
var newImageAttributes: NewImageAttributes? = null
|
||||
|
||||
val attachmentFile = try {
|
||||
File(attachment.path)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
contentUploadStateTracker.setFailure(params.event.eventId, e)
|
||||
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
|
||||
return Result.success(
|
||||
WorkerParamsFactory.toData(params.copy(
|
||||
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 uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
||||
|
@ -88,14 +131,14 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||
ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData ->
|
||||
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
contentUploadStateTracker.setProgressThumbnail(eventId, current, total)
|
||||
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val contentUploadResponse = if (params.isRoomEncrypted) {
|
||||
Timber.v("Encrypt thumbnail")
|
||||
contentUploadStateTracker.setEncryptingThumbnail(eventId)
|
||||
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
|
||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
||||
|
@ -118,10 +161,12 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||
|
||||
val progressListener = object : ProgressRequestBody.Listener {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
if (isStopped) {
|
||||
contentUploadStateTracker.setFailure(eventId, Throwable("Cancelled"))
|
||||
} else {
|
||||
contentUploadStateTracker.setProgress(eventId, current, total)
|
||||
notifyTracker(params) {
|
||||
if (isStopped) {
|
||||
contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
|
||||
} else {
|
||||
contentUploadStateTracker.setProgress(it, current, total)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -131,7 +176,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||
return try {
|
||||
val contentUploadResponse = if (params.isRoomEncrypted) {
|
||||
Timber.v("Encrypt file")
|
||||
contentUploadStateTracker.setEncrypting(eventId)
|
||||
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
|
||||
|
||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
|
||||
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||
|
@ -143,7 +188,12 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||
.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) {
|
||||
Timber.e(t)
|
||||
handleFailure(params, t)
|
||||
|
@ -151,7 +201,8 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||
}
|
||||
|
||||
private fun handleFailure(params: Params, failure: Throwable): Result {
|
||||
contentUploadStateTracker.setFailure(params.event.eventId!!, failure)
|
||||
notifyTracker(params) { contentUploadStateTracker.setFailure(it, failure) }
|
||||
|
||||
return Result.success(
|
||||
WorkerParamsFactory.toData(
|
||||
params.copy(
|
||||
|
@ -165,11 +216,17 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||
attachmentUrl: String,
|
||||
encryptedFileInfo: EncryptedFileInfo?,
|
||||
thumbnailUrl: String?,
|
||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?): Result {
|
||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?,
|
||||
newImageAttributes: NewImageAttributes?): Result {
|
||||
Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped")
|
||||
contentUploadStateTracker.setSuccess(params.event.eventId!!)
|
||||
val event = updateEvent(params.event, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
|
||||
val sendParams = SendEventWorker.Params(params.sessionId, params.roomId, event)
|
||||
notifyTracker(params) { contentUploadStateTracker.setSuccess(it) }
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -177,10 +234,11 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||
url: String,
|
||||
encryptedFileInfo: EncryptedFileInfo?,
|
||||
thumbnailUrl: String? = null,
|
||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?): Event {
|
||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?,
|
||||
newImageAttributes: NewImageAttributes?): Event {
|
||||
val messageContent: MessageContent = event.content.toModel() ?: return event
|
||||
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 MessageFileContent -> 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())
|
||||
}
|
||||
|
||||
private fun notifyTracker(params: Params, function: (String) -> Unit) {
|
||||
params.events
|
||||
.mapNotNull { it.eventId }
|
||||
.forEach { eventId -> function.invoke(eventId) }
|
||||
}
|
||||
|
||||
private fun MessageImageContent.update(url: String,
|
||||
encryptedFileInfo: EncryptedFileInfo?): MessageImageContent {
|
||||
encryptedFileInfo: EncryptedFileInfo?,
|
||||
newImageAttributes: NewImageAttributes?): MessageImageContent {
|
||||
return copy(
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
// Same parameter
|
||||
val params = EncryptEventWorker.Params(sessionId, roomId, event, keepKeys)
|
||||
val params = EncryptEventWorker.Params(sessionId, event, keepKeys)
|
||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||
return timeLineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true)
|
||||
}
|
||||
|
||||
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)
|
||||
return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import androidx.work.OneTimeWorkRequest
|
|||
import androidx.work.Operation
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
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.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
|
@ -49,7 +48,6 @@ import java.util.concurrent.Executors
|
|||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val UPLOAD_WORK = "UPLOAD_WORK"
|
||||
private const val BACKOFF_DELAY = 10_000L
|
||||
|
||||
internal class DefaultSendService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
|
@ -58,7 +56,6 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
@SessionId private val sessionId: String,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val cryptoService: CryptoService,
|
||||
private val monarchy: Monarchy,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val localEchoRepository: LocalEchoRepository
|
||||
) : SendService {
|
||||
|
@ -103,6 +100,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
return if (cryptoService.isRoomEncrypted(roomId)) {
|
||||
Timber.v("Send event in encrypted room")
|
||||
val encryptWork = createEncryptEventWork(event, true)
|
||||
// Note that event will be replaced by the result of the previous work
|
||||
val sendWork = createSendEventWork(event, false)
|
||||
timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
|
||||
} 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()) {
|
||||
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
|
||||
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
|
||||
createLocalEcho(it)
|
||||
// Ensure current roomId is included in the set
|
||||
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)
|
||||
val sendWork = createSendEventWork(localEcho, false)
|
||||
allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) }
|
||||
.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 encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/)
|
||||
val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted)
|
||||
|
||||
val op: Operation = workManagerProvider.workManager
|
||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||
.then(encryptWork)
|
||||
.then(sendWork)
|
||||
.enqueue()
|
||||
op.result.addListener(Runnable {
|
||||
if (op.result.isCancelled) {
|
||||
Timber.e("CHAIN WAS CANCELLED")
|
||||
} else if (op.state.value is Operation.State.FAILURE) {
|
||||
Timber.e("CHAIN DID FAIL")
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||
.then(dispatcherWork)
|
||||
.enqueue()
|
||||
.also { operation ->
|
||||
operation.result.addListener(Runnable {
|
||||
if (operation.result.isCancelled) {
|
||||
Timber.e("CHAIN WAS CANCELLED")
|
||||
} else if (operation.state.value is Operation.State.FAILURE) {
|
||||
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) {
|
||||
|
@ -250,19 +263,19 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
|
||||
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
// Same parameter
|
||||
val params = EncryptEventWorker.Params(sessionId, roomId, event)
|
||||
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, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
|
@ -277,18 +290,33 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
return timelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData, true)
|
||||
}
|
||||
|
||||
private fun createUploadMediaWork(event: Event,
|
||||
private fun createUploadMediaWork(allLocalEchos: List<Event>,
|
||||
attachment: ContentAttachmentData,
|
||||
isRoomEncrypted: Boolean,
|
||||
compressBeforeSending: Boolean,
|
||||
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)
|
||||
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.startChain(startChain)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,9 +38,8 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
val roomId: String,
|
||||
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,
|
||||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams
|
||||
|
@ -52,7 +51,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||
Timber.v("Start Encrypt work")
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: 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}")
|
||||
|
@ -79,7 +78,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||
var result: MXEncryptEventContentResult? = null
|
||||
try {
|
||||
result = awaitCallback {
|
||||
crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
|
||||
crypto.encryptEventContent(localMutableContent, localEvent.type, localEvent.roomId!!, it)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
error = throwable
|
||||
|
@ -97,7 +96,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||
type = safeResult.eventType,
|
||||
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))
|
||||
} else {
|
||||
val sendState = when (error) {
|
||||
|
@ -106,7 +105,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||
}
|
||||
localEchoUpdater.updateSendState(localEvent.eventId, sendState)
|
||||
// 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")
|
||||
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 com.squareup.moshi.JsonClass
|
||||
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.room.send.SendState
|
||||
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.getSessionComponent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SendEventWorker(context: Context,
|
||||
|
@ -39,7 +39,6 @@ internal class SendEventWorker(context: Context,
|
|||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
val roomId: String,
|
||||
val event: Event,
|
||||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams
|
||||
|
@ -50,7 +49,9 @@ internal class SendEventWorker(context: Context,
|
|||
|
||||
override suspend fun doWork(): Result {
|
||||
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()
|
||||
sessionComponent.inject(this)
|
||||
|
@ -66,7 +67,7 @@ internal class SendEventWorker(context: Context,
|
|||
return Result.success(inputData)
|
||||
}
|
||||
return try {
|
||||
sendEvent(event.eventId, event.type, event.content, params.roomId)
|
||||
sendEvent(event)
|
||||
Result.success()
|
||||
} catch (exception: Throwable) {
|
||||
if (exception.shouldBeRetried()) {
|
||||
|
@ -79,16 +80,16 @@ internal class SendEventWorker(context: Context,
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) {
|
||||
localEchoUpdater.updateSendState(eventId, SendState.SENDING)
|
||||
private suspend fun sendEvent(event: Event) {
|
||||
localEchoUpdater.updateSendState(event.eventId!!, SendState.SENDING)
|
||||
executeRequest<SendResponse>(eventBus) {
|
||||
apiCall = roomAPI.send(
|
||||
eventId,
|
||||
roomId,
|
||||
eventType,
|
||||
content
|
||||
event.eventId,
|
||||
event.roomId!!,
|
||||
event.type,
|
||||
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"
|
||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||
implementation 'com.danikula:videocache:2.7.1'
|
||||
implementation 'com.github.yalantis:ucrop:2.2.4'
|
||||
|
||||
// Badge for compatibility
|
||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||
|
|
|
@ -88,7 +88,13 @@
|
|||
</intent-filter>
|
||||
</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>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<data android:mimeType="*/*" />
|
||||
|
@ -134,6 +140,14 @@
|
|||
|
||||
<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 -->
|
||||
|
||||
<service
|
||||
|
|
|
@ -315,6 +315,11 @@ SOFTWARE.
|
|||
<br/>
|
||||
Copyright (c) 2012-2016 Dan Wheeler and Dropbox, Inc.
|
||||
</li>
|
||||
<li>
|
||||
<b>Compressor</b>
|
||||
<br/>
|
||||
Copyright (c) 2016 Zetra.
|
||||
</li>
|
||||
<li>
|
||||
<b>com.otaliastudios:autocomplete</b>
|
||||
<br/>
|
||||
|
@ -375,6 +380,11 @@ SOFTWARE.
|
|||
<br/>
|
||||
Copyright (c) 2014 Dushyanth Maguluru
|
||||
</li>
|
||||
<li>
|
||||
<b>uCrop</b>
|
||||
<br/>
|
||||
Copyright 2017, Yalantis
|
||||
</li>
|
||||
<li>
|
||||
<b>BillCarsonFr/JsonViewer</b>
|
||||
</li>
|
||||
|
|
|
@ -22,6 +22,7 @@ import androidx.fragment.app.FragmentFactory
|
|||
import dagger.Binds
|
||||
import dagger.Module
|
||||
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.CreateDirectRoomKnownUsersFragment
|
||||
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.ignored.VectorSettingsIgnoredUsersFragment
|
||||
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
||||
import im.vector.riotx.features.share.IncomingShareFragment
|
||||
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
|
||||
|
||||
@Module
|
||||
|
@ -350,6 +352,16 @@ interface FragmentModule {
|
|||
@FragmentKey(CrossSigningSettingsFragment::class)
|
||||
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
|
||||
@IntoMap
|
||||
@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.session.SessionListener
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import im.vector.riotx.features.share.ShareRoomListDataSource
|
||||
import im.vector.riotx.features.ui.UiStateRepository
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -97,8 +96,6 @@ interface VectorComponent {
|
|||
|
||||
fun homeRoomListObservableStore(): HomeRoomListDataSource
|
||||
|
||||
fun shareRoomListObservableStore(): ShareRoomListDataSource
|
||||
|
||||
fun selectedGroupStore(): SelectedGroupDataSource
|
||||
|
||||
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
|
||||
* ========================================================================================== */
|
||||
|
|
|
@ -26,6 +26,7 @@ import javax.inject.Inject
|
|||
|
||||
class ColorProvider @Inject constructor(private val context: Context) {
|
||||
|
||||
@ColorInt
|
||||
fun getColor(@ColorRes colorRes: Int): Int {
|
||||
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_PICK_ATTACHMENT = 576
|
||||
const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577
|
||||
const val PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT = 578
|
||||
|
||||
/**
|
||||
* 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.os.Bundle
|
||||
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 im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.riotx.core.platform.Restorable
|
||||
import im.vector.riotx.features.attachments.AttachmentsHelper.Callback
|
||||
import timber.log.Timber
|
||||
|
||||
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
|
||||
|
|
|
@ -16,7 +16,11 @@
|
|||
|
||||
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 timber.log.Timber
|
||||
|
||||
|
@ -37,7 +41,8 @@ fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
|
|||
type = mapType(),
|
||||
size = size,
|
||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||
name = displayName
|
||||
name = displayName,
|
||||
queryUri = queryUri
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -50,7 +55,8 @@ fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
|
|||
size = size,
|
||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||
name = displayName,
|
||||
duration = duration
|
||||
duration = duration,
|
||||
queryUri = queryUri
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -74,7 +80,8 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
|
|||
height = height.toLong(),
|
||||
width = width.toLong(),
|
||||
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(),
|
||||
width = width.toLong(),
|
||||
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.ImagePickerCallback
|
||||
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]
|
||||
|
|
|
@ -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");
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* 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.utils.BehaviorDataSource
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import im.vector.riotx.core.platform.VectorViewEvents
|
||||
|
||||
@Singleton
|
||||
class ShareRoomListDataSource @Inject constructor() : BehaviorDataSource<List<RoomSummary>>()
|
||||
sealed class AttachmentsPreviewViewEvents : VectorViewEvents
|
|
@ -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>) {
|
||||
vectorBaseActivity.invalidateOptionsMenu()
|
||||
invalidateOptionsMenu()
|
||||
if (selectedUsers.isNotEmpty() && chipGroup.size == 0) {
|
||||
selectedUsers.forEach { addChipToGroup(it, chipGroup) }
|
||||
}
|
||||
|
|
|
@ -23,6 +23,5 @@ enum class RoomListDisplayMode(@StringRes val titleRes: Int) {
|
|||
HOME(R.string.bottom_action_home),
|
||||
PEOPLE(R.string.bottom_action_people_x),
|
||||
ROOMS(R.string.bottom_action_rooms),
|
||||
FILTERED(/* Not used */ 0),
|
||||
SHARE(/* Not used */ 0)
|
||||
FILTERED(/* Not used */ 0)
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
|
||||
data class SaveDraft(val draft: String) : 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 TimelineEventTurnsInvisible(val event: TimelineEvent) : 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.AttachmentsHelper
|
||||
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.crypto.util.toImageRes
|
||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
||||
|
@ -299,10 +302,15 @@ class RoomDetailFragment @Inject constructor(
|
|||
super.onActivityCreated(savedInstanceState)
|
||||
if (savedInstanceState == null) {
|
||||
when (val sharedData = roomDetailArgs.sharedData) {
|
||||
is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false))
|
||||
is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData))
|
||||
null -> Timber.v("No share data to process")
|
||||
}
|
||||
is SharedData.Text -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ExitSpecialMode(composerLayout.text.toString()))
|
||||
}
|
||||
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)
|
||||
if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
|
||||
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
|
||||
roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction))
|
||||
}
|
||||
|
@ -637,9 +650,11 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun sendUri(uri: Uri): Boolean {
|
||||
roomDetailViewModel.preventAttachmentPreview = true
|
||||
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
||||
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
|
||||
if (!isHandled) {
|
||||
roomDetailViewModel.preventAttachmentPreview = false
|
||||
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return isHandled
|
||||
|
@ -1334,10 +1349,24 @@ class RoomDetailFragment @Inject constructor(
|
|||
// AttachmentsHelper.Callback
|
||||
|
||||
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() {
|
||||
roomDetailViewModel.preventAttachmentPreview = false
|
||||
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
|
||||
// Slot to keep a pending uri during permission request
|
||||
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 mostRecentDisplayedEvent: TimelineEvent? = null
|
||||
|
@ -582,10 +584,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
|
||||
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
|
||||
// Unknown limitation
|
||||
room.sendMedias(attachments)
|
||||
room.sendMedias(attachments, action.compressBeforeSending, emptySet())
|
||||
} else {
|
||||
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
||||
null -> room.sendMedias(attachments)
|
||||
null -> room.sendMedias(attachments, action.compressBeforeSending, emptySet())
|
||||
else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError(
|
||||
tooBigFile.name ?: tooBigFile.path,
|
||||
tooBigFile.size,
|
||||
|
|
|
@ -33,7 +33,6 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListDisplayMode) :
|
|||
RoomListDisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
|
||||
RoomListDisplayMode.ROOMS -> !roomSummary.isDirect && 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.widget.FabMenuView
|
||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||
import im.vector.riotx.features.share.SharedData
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_room_list.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
data class RoomListParams(
|
||||
val displayMode: RoomListDisplayMode,
|
||||
val sharedData: SharedData? = null
|
||||
val displayMode: RoomListDisplayMode
|
||||
) : Parcelable
|
||||
|
||||
class RoomListFragment @Inject constructor(
|
||||
|
@ -106,7 +104,7 @@ class RoomListFragment @Inject constructor(
|
|||
when (it) {
|
||||
is RoomListViewEvents.Loading -> showLoading(it.message)
|
||||
is RoomListViewEvents.Failure -> showFailure(it.throwable)
|
||||
is RoomListViewEvents.SelectRoom -> openSelectedRoom(it)
|
||||
is RoomListViewEvents.SelectRoom -> handleSelectRoom(it)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
@ -131,13 +129,8 @@ class RoomListFragment @Inject constructor(
|
|||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) {
|
||||
if (roomListParams.displayMode == RoomListDisplayMode.SHARE) {
|
||||
val sharedData = roomListParams.sharedData ?: return
|
||||
navigator.openRoomForSharing(requireActivity(), event.roomId, sharedData)
|
||||
} else {
|
||||
navigator.openRoom(requireActivity(), event.roomId)
|
||||
}
|
||||
private fun handleSelectRoom(event: RoomListViewEvents.SelectRoom) {
|
||||
navigator.openRoom(requireActivity(), event.roomSummary.roomId)
|
||||
}
|
||||
|
||||
private fun setupCreateRoomButton() {
|
||||
|
@ -256,7 +249,6 @@ class RoomListFragment @Inject constructor(
|
|||
is Fail -> renderFailure(state.asyncFilteredRooms.error)
|
||||
}
|
||||
roomController.update(state)
|
||||
|
||||
// Mark all as read menu
|
||||
when (roomListParams.displayMode) {
|
||||
RoomListDisplayMode.HOME,
|
||||
|
@ -265,7 +257,7 @@ class RoomListFragment @Inject constructor(
|
|||
val newValue = state.hasUnread
|
||||
if (hasUnreadRooms != newValue) {
|
||||
hasUnreadRooms = newValue
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
|
@ -338,7 +330,6 @@ class RoomListFragment @Inject constructor(
|
|||
if (createChatFabMenu.onBackPressed()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -350,7 +341,6 @@ class RoomListFragment @Inject constructor(
|
|||
|
||||
override fun onRoomLongClicked(room: RoomSummary): Boolean {
|
||||
roomController.onRoomLongClicked()
|
||||
|
||||
RoomListQuickActionsBottomSheet
|
||||
.newInstance(room.roomId, RoomListActionsArgs.Mode.FULL)
|
||||
.show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS")
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
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
|
||||
|
||||
/**
|
||||
|
@ -26,5 +27,5 @@ sealed class RoomListViewEvents : VectorViewEvents {
|
|||
data class Loading(val message: CharSequence? = null) : 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.RoomSummary
|
||||
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.utils.DataSource
|
||||
import im.vector.riotx.features.home.RoomListDisplayMode
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -67,13 +67,13 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
|
|||
is RoomListAction.MarkAllRoomsRead -> handleMarkAllRoomsRead()
|
||||
is RoomListAction.LeaveRoom -> handleLeaveRoom(action)
|
||||
is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun handleSelectRoom(action: RoomListAction.SelectRoom) {
|
||||
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary.roomId))
|
||||
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
|
||||
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary))
|
||||
}
|
||||
|
||||
private fun handleToggleCategory(action: RoomListAction.ToggleCategory) = setState {
|
||||
|
@ -204,54 +204,35 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
|
|||
}
|
||||
|
||||
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
|
||||
if (displayMode == RoomListDisplayMode.SHARE) {
|
||||
val recentRooms = ArrayList<RoomSummary>(20)
|
||||
val otherRooms = ArrayList<RoomSummary>(rooms.size)
|
||||
// Set up init size on directChats and groupRooms as they are the biggest ones
|
||||
val invites = ArrayList<RoomSummary>()
|
||||
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 ->
|
||||
when (room.breadcrumbsIndex) {
|
||||
RoomSummary.NOT_IN_BREADCRUMBS -> otherRooms.add(room)
|
||||
else -> recentRooms.add(room)
|
||||
}
|
||||
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.RECENT_ROOMS, recentRooms)
|
||||
put(RoomCategory.OTHER_ROOMS, otherRooms)
|
||||
}
|
||||
} else {
|
||||
// Set up init size on directChats and groupRooms as they are the biggest ones
|
||||
val invites = ArrayList<RoomSummary>()
|
||||
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)
|
||||
}
|
||||
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.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.Provider
|
||||
|
||||
class RoomListViewModelFactory @Inject constructor(private val session: Provider<Session>,
|
||||
private val homeRoomListDataSource: Provider<HomeRoomListDataSource>,
|
||||
private val shareRoomListDataSource: Provider<ShareRoomListDataSource>)
|
||||
private val homeRoomListDataSource: Provider<HomeRoomListDataSource>)
|
||||
: RoomListViewModel.Factory {
|
||||
|
||||
override fun create(initialState: RoomListViewState): RoomListViewModel {
|
||||
return RoomListViewModel(
|
||||
initialState,
|
||||
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 isGroupRoomsExpanded: Boolean = true,
|
||||
val isLowPriorityRoomsExpanded: Boolean = true,
|
||||
val isServerNoticeRoomsExpanded: Boolean = true,
|
||||
// For sharing
|
||||
val isRecentExpanded: Boolean = true,
|
||||
val isOtherExpanded: Boolean = true
|
||||
val isServerNoticeRoomsExpanded: Boolean = true
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomListParams) : this(displayMode = args.displayMode)
|
||||
|
@ -59,8 +56,6 @@ data class RoomListViewState(
|
|||
RoomCategory.GROUP -> isGroupRoomsExpanded
|
||||
RoomCategory.LOW_PRIORITY -> isLowPriorityRoomsExpanded
|
||||
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.LOW_PRIORITY -> copy(isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded)
|
||||
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),
|
||||
GROUP(R.string.bottom_action_rooms),
|
||||
LOW_PRIORITY(R.string.low_priority_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)
|
||||
SERVER_NOTICE(R.string.system_alerts_header)
|
||||
}
|
||||
|
||||
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.riotx.R
|
||||
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.UserPreferencesProvider
|
||||
import im.vector.riotx.features.home.RoomListDisplayMode
|
||||
|
@ -60,7 +59,6 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
|
|||
val nonNullViewState = viewState ?: return
|
||||
when (nonNullViewState.displayMode) {
|
||||
RoomListDisplayMode.FILTERED -> buildFilteredRooms(nonNullViewState)
|
||||
RoomListDisplayMode.SHARE -> buildShareRooms(nonNullViewState)
|
||||
else -> buildRooms(nonNullViewState)
|
||||
}
|
||||
}
|
||||
|
@ -77,44 +75,12 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
|
|||
viewState.joiningRoomsIds,
|
||||
viewState.joiningErrorRoomsIds,
|
||||
viewState.rejectingRoomsIds,
|
||||
viewState.rejectingErrorRoomsIds)
|
||||
viewState.rejectingErrorRoomsIds,
|
||||
emptySet())
|
||||
|
||||
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) {
|
||||
var showHelp = false
|
||||
val roomSummaries = viewState.asyncFilteredRooms()
|
||||
|
@ -131,7 +97,8 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
|
|||
viewState.joiningRoomsIds,
|
||||
viewState.joiningErrorRoomsIds,
|
||||
viewState.rejectingRoomsIds,
|
||||
viewState.rejectingErrorRoomsIds)
|
||||
viewState.rejectingErrorRoomsIds,
|
||||
emptySet())
|
||||
// Never set showHelp to true for invitation
|
||||
if (category != RoomCategory.INVITE) {
|
||||
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,
|
||||
summaries: List<RoomSummary>,
|
||||
@StringRes titleRes: Int,
|
||||
|
@ -196,10 +156,17 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
|
|||
joiningRoomsIds: Set<String>,
|
||||
joiningErrorRoomsIds: Set<String>,
|
||||
rejectingRoomsIds: Set<String>,
|
||||
rejectingErrorRoomsIds: Set<String>) {
|
||||
rejectingErrorRoomsIds: Set<String>,
|
||||
selectedRoomIds: Set<String>) {
|
||||
summaries.forEach { roomSummary ->
|
||||
roomSummaryItemFactory
|
||||
.create(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
|
||||
.create(roomSummary,
|
||||
joiningRoomsIds,
|
||||
joiningErrorRoomsIds,
|
||||
rejectingRoomsIds,
|
||||
rejectingErrorRoomsIds,
|
||||
selectedRoomIds,
|
||||
listener)
|
||||
.addTo(this)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,14 +16,17 @@
|
|||
|
||||
package im.vector.riotx.features.home.room.list
|
||||
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.riotx.R
|
||||
|
@ -48,11 +51,15 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||
@EpoxyAttribute var showHighlighted: Boolean = false
|
||||
@EpoxyAttribute var itemLongClickListener: View.OnLongClickListener? = null
|
||||
@EpoxyAttribute var itemClickListener: View.OnClickListener? = null
|
||||
@EpoxyAttribute var showSelected: Boolean = false
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
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.lastEventTimeView.text = lastEventTime
|
||||
holder.lastEventView.text = lastFormattedEvent
|
||||
|
@ -64,6 +71,19 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
||||
holder.roomAvatarDecorationImageView.isVisible = encryptionTrustLevel != null
|
||||
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() {
|
||||
|
@ -74,6 +94,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||
val typingView by bind<TextView>(R.id.roomTypingView)
|
||||
val draftView by bind<ImageView>(R.id.roomDraftBadge)
|
||||
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 roomAvatarDecorationImageView by bind<ImageView>(R.id.roomAvatarDecorationImageView)
|
||||
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.epoxy.VectorEpoxyModel
|
||||
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.StringProvider
|
||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||
|
@ -36,7 +35,6 @@ import javax.inject.Inject
|
|||
|
||||
class RoomSummaryItemFactory @Inject constructor(private val displayableEventFormatter: DisplayableEventFormatter,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val stringProvider: StringProvider,
|
||||
private val typingHelper: TypingHelper,
|
||||
private val session: Session,
|
||||
|
@ -47,19 +45,20 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
|||
joiningErrorRoomsIds: Set<String>,
|
||||
rejectingRoomsIds: Set<String>,
|
||||
rejectingErrorRoomsIds: Set<String>,
|
||||
selectedRoomIds: Set<String>,
|
||||
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
|
||||
return when (roomSummary.membership) {
|
||||
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,
|
||||
joiningRoomsIds: Set<String>,
|
||||
joiningErrorRoomsIds: Set<String>,
|
||||
rejectingRoomsIds: Set<String>,
|
||||
rejectingErrorRoomsIds: Set<String>,
|
||||
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
|
||||
fun createInvitationItem(roomSummary: RoomSummary,
|
||||
joiningRoomsIds: Set<String>,
|
||||
joiningErrorRoomsIds: Set<String>,
|
||||
rejectingRoomsIds: Set<String>,
|
||||
rejectingErrorRoomsIds: Set<String>,
|
||||
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
|
||||
val secondLine = if (roomSummary.isDirect) {
|
||||
roomSummary.latestPreviewableEvent?.root?.senderId
|
||||
} else {
|
||||
|
@ -82,10 +81,15 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
|||
.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 showHighlighted = roomSummary.highlightCount > 0
|
||||
|
||||
val showSelected = selectedRoomIds.contains(roomSummary.roomId)
|
||||
var latestFormattedEvent: CharSequence = ""
|
||||
var latestEventTime: CharSequence = ""
|
||||
val latestEvent = roomSummary.latestPreviewableEvent
|
||||
|
@ -119,15 +123,16 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
|||
.typingString(typingString)
|
||||
.lastFormattedEvent(latestFormattedEvent)
|
||||
.showHighlighted(showHighlighted)
|
||||
.showSelected(showSelected)
|
||||
.unreadNotificationCount(unreadCount)
|
||||
.hasUnreadMessage(roomSummary.hasUnreadMessages)
|
||||
.hasDraft(roomSummary.userDrafts.isNotEmpty())
|
||||
.itemLongClickListener { _ ->
|
||||
listener?.onRoomLongClicked(roomSummary) ?: false
|
||||
onLongClick?.invoke(roomSummary) ?: false
|
||||
}
|
||||
.itemClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { _ ->
|
||||
listener?.onRoomClicked(roomSummary)
|
||||
onClick?.invoke(roomSummary)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -64,15 +64,15 @@ class DefaultNavigator @Inject constructor(
|
|||
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 tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransationId) ?: return
|
||||
val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) ?: return
|
||||
(tx as? IncomingSasVerificationTransaction)?.performAccept()
|
||||
if (context is VectorBaseActivity) {
|
||||
VerificationBottomSheet.withArgs(
|
||||
roomId = null,
|
||||
otherUserId = otherUserId,
|
||||
transactionId = sasTransationId
|
||||
transactionId = sasTransactionId
|
||||
).show(context.supportFragmentManager, "REQPOP")
|
||||
}
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ class DefaultNavigator @Inject constructor(
|
|||
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 intent = RoomDetailActivity.newIntent(activity, args)
|
||||
activity.startActivity(intent)
|
||||
|
|
|
@ -26,11 +26,13 @@ interface Navigator {
|
|||
|
||||
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 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)
|
||||
|
||||
|
|
|
@ -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
|
||||
* 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
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.share
|
||||
|
||||
import android.content.ClipDescription
|
||||
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 androidx.appcompat.widget.Toolbar
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.extensions.replaceFragment
|
||||
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.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 :
|
||||
VectorBaseActivity(), AttachmentsHelper.Callback {
|
||||
class IncomingShareActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
|
||||
@Inject lateinit var sessionHolder: ActiveSessionHolder
|
||||
@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_simple
|
||||
|
||||
override fun getLayoutRes() = R.layout.activity_incoming_share
|
||||
|
||||
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)
|
||||
override fun initUiAndData() {
|
||||
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>) {
|
||||
val roomListParams = RoomListParams(RoomListDisplayMode.SHARE, sharedData = SharedData.Attachments(attachments))
|
||||
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()
|
||||
override fun configure(toolbar: Toolbar) {
|
||||
configureToolbar(toolbar, displayBack = false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
import com.airbnb.mvrx.ActivityViewModelContext
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
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.rx.rx
|
||||
import im.vector.riotx.ActiveSessionDataSource
|
||||
import im.vector.riotx.core.platform.EmptyAction
|
||||
import im.vector.riotx.core.platform.EmptyViewEvents
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
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 io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class IncomingShareState(private val dummy: Boolean = false) : MvRxState
|
||||
|
||||
/**
|
||||
* View model used to observe the room list and post update to the ShareRoomListObservableStore
|
||||
*/
|
||||
class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: IncomingShareState,
|
||||
private val sessionObservableStore: ActiveSessionDataSource,
|
||||
private val shareRoomListObservableStore: ShareRoomListDataSource,
|
||||
private val breadcrumbsRoomComparator: BreadcrumbsRoomComparator)
|
||||
: VectorViewModel<IncomingShareState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||
class IncomingShareViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: IncomingShareViewState,
|
||||
private val session: Session,
|
||||
private val breadcrumbsRoomComparator: BreadcrumbsRoomComparator)
|
||||
: VectorViewModel<IncomingShareViewState, IncomingShareAction, IncomingShareViewEvents>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: IncomingShareState): IncomingShareViewModel
|
||||
fun create(initialState: IncomingShareViewState): IncomingShareViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<IncomingShareViewModel, IncomingShareState> {
|
||||
companion object : MvRxViewModelFactory<IncomingShareViewModel, IncomingShareViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: IncomingShareState): IncomingShareViewModel? {
|
||||
val activity: IncomingShareActivity = (viewModelContext as ActivityViewModelContext).activity()
|
||||
return activity.incomingShareViewModelFactory.create(state)
|
||||
override fun create(viewModelContext: ViewModelContext, state: IncomingShareViewState): IncomingShareViewModel? {
|
||||
val fragment: IncomingShareFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.incomingShareViewModelFactory.create(state)
|
||||
}
|
||||
}
|
||||
|
||||
private val filterStream: BehaviorRelay<String> = BehaviorRelay.createDefault("")
|
||||
|
||||
init {
|
||||
observeRoomSummaries()
|
||||
}
|
||||
|
||||
private fun observeRoomSummaries() {
|
||||
val queryParams = roomSummaryQueryParams()
|
||||
sessionObservableStore.observe()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.switchMap {
|
||||
it.orNull()?.rx()?.liveRoomSummaries(queryParams)
|
||||
?: Observable.just(emptyList())
|
||||
val queryParams = roomSummaryQueryParams {
|
||||
memberships = listOf(Membership.JOIN)
|
||||
}
|
||||
session
|
||||
.rx().liveRoomSummaries(queryParams)
|
||||
.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)
|
||||
.map {
|
||||
it.sortedWith(breadcrumbsRoomComparator)
|
||||
.map { it.sortedWith(breadcrumbsRoomComparator) }
|
||||
.execute {
|
||||
copy(filteredRoomSummaries = it)
|
||||
}
|
||||
.subscribe {
|
||||
shareRoomListObservableStore.post(it)
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
override fun handle(action: EmptyAction) {
|
||||
// No op
|
||||
override fun handle(action: IncomingShareAction) {
|
||||
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_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"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:elevation="4dp"
|
||||
app:contentInsetStart="0dp"
|
||||
app:layout_constraintEnd_toEndOf="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"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/roomAvatarImageView"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
<FrameLayout
|
||||
android:id="@+id/roomAvatarContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<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
|
||||
android:id="@+id/roomAvatarDecorationImageView"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
app:layout_constraintCircle="@+id/roomAvatarImageView"
|
||||
app:layout_constraintCircle="@id/roomAvatarContainer"
|
||||
app:layout_constraintCircleAngle="135"
|
||||
app:layout_constraintCircleRadius="28dp"
|
||||
tools:ignore="MissingConstraints"
|
||||
|
@ -47,7 +63,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/roomAvatarImageView"
|
||||
app:layout_constraintTop_toBottomOf="@id/roomAvatarContainer"
|
||||
tools:layout_marginStart="20dp" />
|
||||
|
||||
<im.vector.riotx.core.platform.EllipsizingTextView
|
||||
|
@ -69,7 +85,7 @@
|
|||
app:layout_constraintEnd_toStartOf="@+id/roomDraftBadge"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@id/roomAvatarImageView"
|
||||
app:layout_constraintStart_toEndOf="@id/roomAvatarContainer"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
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 -->
|
||||
<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>
|
||||
<!-- END Strings added by Benoit -->
|
||||
|
||||
<!-- BEGIN Strings added by Benoit -->
|
||||
|
||||
<!-- END Strings added by Benoit -->
|
||||
|
|
|
@ -8,4 +8,6 @@
|
|||
<item name="colorPrimaryDark">@color/primary_color_dark</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.AttachmentsPreview" parent="AppTheme.Base.Black"/>
|
||||
|
||||
</resources>
|
Loading…
Reference in a new issue