Unverified Commit c448359d authored by Lucio Maciel's avatar Lucio Maciel Committed by GitHub

Merge pull request #1384 from RocketChat/feature/compress-image

[NEW] Compress image
parents 18e06ae0 590aedde
......@@ -5,26 +5,32 @@ import android.net.Uri
import chat.rocket.android.util.extensions.*
import javax.inject.Inject
class UriInteractor @Inject constructor(private val context: Context) {
/**
* Gets the file name from an [Uri].
* Returns the file name from the [Uri].
*/
fun getFileName(uri: Uri): String? = uri.getFileName(context)
/**
* Gets the MimeType of an [Uri]
* Returns the MimeType from the [Uri].
*/
fun getMimeType(uri: Uri): String = uri.getMimeType(context)
/**
* Gets the real path of an [Uri]
* Returns the file size from the [Uri].
*/
fun getRealPath(uri: Uri): String? = uri.getRealPathFromURI(context)
fun getFileSize(uri: Uri) = uri.getFileSize(context)
/**
* Returns the InputStream from the [Uri].
*/
fun getInputStream(uri: Uri) = uri.getInputStream(context)
/**
* Returns the Bitmap from the [Uri].
*
* Note: It should be an image.
*/
fun getBitmap(uri: Uri) = uri.getBitmpap(context)
}
\ No newline at end of file
......@@ -16,21 +16,11 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.MessageHelper
import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.JobSchedulerInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.RoomRepository
import chat.rocket.android.server.domain.UsersRepository
import chat.rocket.android.server.domain.uploadMaxFileSize
import chat.rocket.android.server.domain.uploadMimeTypeFilter
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.getCompressFormat
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
......@@ -43,36 +33,22 @@ import chat.rocket.core.internal.realtime.setTypingStatus
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.realtime.subscribeTypingStatus
import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.rest.chatRoomRoles
import chat.rocket.core.internal.rest.commands
import chat.rocket.core.internal.rest.deleteMessage
import chat.rocket.core.internal.rest.getMembers
import chat.rocket.core.internal.rest.history
import chat.rocket.core.internal.rest.joinChat
import chat.rocket.core.internal.rest.markAsRead
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.messages
import chat.rocket.core.internal.rest.pinMessage
import chat.rocket.core.internal.rest.runCommand
import chat.rocket.core.internal.rest.sendMessage
import chat.rocket.core.internal.rest.spotlight
import chat.rocket.core.internal.rest.starMessage
import chat.rocket.core.internal.rest.toggleReaction
import chat.rocket.core.internal.rest.unpinMessage
import chat.rocket.core.internal.rest.unstarMessage
import chat.rocket.core.internal.rest.updateMessage
import chat.rocket.core.internal.rest.uploadFile
import chat.rocket.core.internal.rest.*
import chat.rocket.core.model.ChatRoomRole
import chat.rocket.core.model.Command
import chat.rocket.core.model.Message
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import org.threeten.bp.Instant
import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.util.*
import javax.inject.Inject
......@@ -111,7 +87,12 @@ class ChatRoomPresenter @Inject constructor(
private var lastState = manager.state
private var typingStatusList = arrayListOf<String>()
fun setupChatRoom(roomId: String, roomName: String, roomType: String, chatRoomMessage: String? = null) {
fun setupChatRoom(
roomId: String,
roomName: String,
roomType: String,
chatRoomMessage: String? = null
) {
launchUI(strategy) {
try {
chatRoles = if (roomTypeOf(roomType) !is RoomType.DirectMessage) {
......@@ -130,9 +111,15 @@ class ChatRoomPresenter @Inject constructor(
} ?: false
view.onRoomUpdated(userCanPost, chatIsBroadcast, userCanMod)
loadMessages(roomId, roomType)
chatRoomMessage?.let { messageHelper.messageIdFromPermalink(it) }?.let { messageId ->
chatRoomMessage?.let { messageHelper.messageIdFromPermalink(it) }
?.let { messageId ->
val name = messageHelper.roomNameFromPermalink(chatRoomMessage)
citeMessage(name!!, messageHelper.roomTypeFromPermalink(chatRoomMessage)!!, messageId, true)
citeMessage(
name!!,
messageHelper.roomTypeFromPermalink(chatRoomMessage)!!,
messageId,
true
)
}
}
}
......@@ -152,8 +139,12 @@ class ChatRoomPresenter @Inject constructor(
try {
if (offset == 0L) {
val localMessages = messagesRepository.getByRoomId(chatRoomId)
val oldMessages = mapper.map(localMessages, RoomUiModel(roles = chatRoles,
isBroadcast = chatIsBroadcast, isRoom = true))
val oldMessages = mapper.map(
localMessages, RoomUiModel(
roles = chatRoles,
isBroadcast = chatIsBroadcast, isRoom = true
)
)
if (oldMessages.isNotEmpty()) {
view.showMessages(oldMessages)
loadMissingMessages()
......@@ -185,14 +176,24 @@ class ChatRoomPresenter @Inject constructor(
}
}
private suspend fun loadAndShowMessages(chatRoomId: String, chatRoomType: String, offset: Long = 0) {
private suspend fun loadAndShowMessages(
chatRoomId: String,
chatRoomType: String,
offset: Long = 0
) {
val messages =
retryIO(description = "messages chatRoom: $chatRoomId, type: $chatRoomType, offset: $offset") {
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
}
messagesRepository.saveAll(messages)
view.showMessages(mapper.map(messages, RoomUiModel(roles = chatRoles,
isBroadcast = chatIsBroadcast, isRoom = true)))
view.showMessages(
mapper.map(
messages, RoomUiModel(
roles = chatRoles,
isBroadcast = chatIsBroadcast, isRoom = true
)
)
)
}
fun sendMessage(chatRoomId: String, text: String, messageId: String?) {
......@@ -227,8 +228,13 @@ class ChatRoomPresenter @Inject constructor(
)
try {
messagesRepository.save(newMessage)
view.showNewMessage(mapper.map(newMessage, RoomUiModel(
roles = chatRoles, isBroadcast = chatIsBroadcast)))
view.showNewMessage(
mapper.map(
newMessage, RoomUiModel(
roles = chatRoles, isBroadcast = chatIsBroadcast
)
)
)
client.sendMessage(id, chatRoomId, text)
} catch (ex: Exception) {
// Ok, not very beautiful, but the backend sends us a not valid response
......@@ -265,19 +271,37 @@ class ChatRoomPresenter @Inject constructor(
launchUI(strategy) {
view.showLoading()
try {
val fileName = async { uriInteractor.getFileName(uri) }.await() ?: uri.toString()
val mimeType = async { uriInteractor.getMimeType(uri) }.await()
val fileSize = async { uriInteractor.getFileSize(uri) }.await()
val maxFileSize = settings.uploadMaxFileSize()
withContext(DefaultDispatcher) {
val fileName = uriInteractor.getFileName(uri) ?: uri.toString()
val fileSize = uriInteractor.getFileSize(uri)
val mimeType = uriInteractor.getMimeType(uri)
val maxFileSizeAllowed = settings.uploadMaxFileSize()
when {
fileName.isNullOrEmpty() -> view.showInvalidFileMessage()
fileSize > maxFileSize -> view.showInvalidFileSize(fileSize, maxFileSize)
fileName.isEmpty() -> {
view.showInvalidFileMessage()
}
fileSize > maxFileSizeAllowed -> {
view.showInvalidFileSize(fileSize, maxFileSizeAllowed)
}
else -> {
Timber.d("Uploading to $roomId: $fileName - $mimeType")
var inputStream: InputStream? = uriInteractor.getInputStream(uri)
if (mimeType.contains("image")) {
compressImage(uri, mimeType)?.let {
inputStream = it
}
}
retryIO("uploadFile($roomId, $fileName, $mimeType") {
client.uploadFile(roomId, fileName!!, mimeType, msg, description = fileName) {
uriInteractor.getInputStream(uri)
client.uploadFile(
roomId,
fileName,
mimeType,
msg,
description = fileName
) {
inputStream
}
}
}
}
......@@ -294,6 +318,25 @@ class ChatRoomPresenter @Inject constructor(
}
}
// Returns an InputStream of a compressed image.
private suspend fun compressImage(uri: Uri, mimeType: String): InputStream? {
var inputStream: InputStream? = null
uriInteractor.getBitmap(uri)?.let {
withContext(DefaultDispatcher) {
val byteArrayOutputStream = ByteArrayOutputStream()
// TODO: Add an option the the app to the user be able to select the quality of the compressed image
val isCompressed =
it.compress(it.getCompressFormat(mimeType), 70, byteArrayOutputStream)
if (isCompressed) {
inputStream = ByteArrayInputStream(byteArrayOutputStream.toByteArray())
}
}
}
return inputStream
}
fun sendTyping() {
launch(CommonPool + strategy.jobs) {
if (chatRoomId != null && currentLoggedUsername != null) {
......@@ -362,15 +405,23 @@ class ChatRoomPresenter @Inject constructor(
.sortedByDescending { it.timestamp }.firstOrNull()?.let { lastMessage ->
val instant = Instant.ofEpochMilli(lastMessage.timestamp).toString()
try {
val messages = retryIO(description = "history($chatRoomId, $roomType, $instant)") {
client.history(chatRoomId!!, roomType, count = 50,
oldest = instant)
val messages =
retryIO(description = "history($chatRoomId, $roomType, $instant)") {
client.history(
chatRoomId!!, roomType, count = 50,
oldest = instant
)
}
Timber.d("History: $messages")
if (messages.result.isNotEmpty()) {
val models = mapper.map(messages.result, RoomUiModel(
roles = chatRoles, isBroadcast = chatIsBroadcast, isRoom = true))
val models = mapper.map(
messages.result, RoomUiModel(
roles = chatRoles,
isBroadcast = chatIsBroadcast,
isRoom = true
)
)
messagesRepository.saveAll(messages.result)
launchUI(strategy) {
......@@ -440,7 +491,8 @@ class ChatRoomPresenter @Inject constructor(
val id = msg.id
val username = msg.sender?.username ?: ""
val mention = if (mentionAuthor && me?.username != username) "@$username" else ""
val room = if (roomTypeOf(roomType) is RoomType.DirectMessage) username else roomName
val room =
if (roomTypeOf(roomType) is RoomType.DirectMessage) username else roomName
val chatRoomType = when (roomTypeOf(roomType)) {
is RoomType.DirectMessage -> "direct"
is RoomType.PrivateGroup -> "group"
......@@ -451,8 +503,12 @@ class ChatRoomPresenter @Inject constructor(
view.showReplyingAction(
username = getDisplayName(msg.sender),
replyMarkdown = "[ ]($currentServer/$chatRoomType/$room?msg=$id) $mention ",
quotedMessage = mapper.map(message, RoomUiModel(roles = chatRoles,
isBroadcast = chatIsBroadcast)).last().preview?.message ?: ""
quotedMessage = mapper.map(
message, RoomUiModel(
roles = chatRoles,
isBroadcast = chatIsBroadcast
)
).last().preview?.message ?: ""
)
}
}
......@@ -553,7 +609,12 @@ class ChatRoomPresenter @Inject constructor(
}
}
fun loadActiveMembers(chatRoomId: String, chatRoomType: String, offset: Long = 0, filterSelfOut: Boolean = false) {
fun loadActiveMembers(
chatRoomId: String,
chatRoomType: String,
offset: Long = 0,
filterSelfOut: Boolean = false
) {
launchUI(strategy) {
try {
val members = retryIO("getMembers($chatRoomId, $chatRoomType, $offset)") {
......@@ -573,8 +634,12 @@ class ChatRoomPresenter @Inject constructor(
val found = members.firstOrNull { member -> member.username == username }
val status = if (found != null) found.status else UserStatus.Offline()
val searchList = mutableListOf(username, name)
activeUsers.add(PeopleSuggestionUiModel(avatarUrl, username, username, name, status,
true, searchList))
activeUsers.add(
PeopleSuggestionUiModel(
avatarUrl, username, username, name, status,
true, searchList
)
)
}
// Filter out from members list the active users.
val others = members.filterNot { member ->
......@@ -588,7 +653,15 @@ class ChatRoomPresenter @Inject constructor(
val name = it.name ?: ""
val avatarUrl = currentServer.avatarUrl(username)
val searchList = mutableListOf(username, name)
PeopleSuggestionUiModel(avatarUrl, username, username, name, it.status, true, searchList)
PeopleSuggestionUiModel(
avatarUrl,
username,
username,
name,
it.status,
true,
searchList
)
})
view.populatePeopleSuggestions(activeUsers)
......@@ -613,8 +686,10 @@ class ChatRoomPresenter @Inject constructor(
val name = it.name ?: ""
val searchList = mutableListOf(username, name)
it.emails?.forEach { email -> searchList.add(email.address) }
PeopleSuggestionUiModel(currentServer.avatarUrl(username),
username, username, name, it.status, false, searchList)
PeopleSuggestionUiModel(
currentServer.avatarUrl(username),
username, username, name, it.status, false, searchList
)
}.filterNot { filterSelfOut && self != null && self == it.text })
}
ROOMS -> {
......@@ -834,8 +909,11 @@ class ChatRoomPresenter @Inject constructor(
private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) {
val viewModelStreamedMessage = mapper.map(streamedMessage, RoomUiModel(
roles = chatRoles, isBroadcast = chatIsBroadcast))
val viewModelStreamedMessage = mapper.map(
streamedMessage, RoomUiModel(
roles = chatRoles, isBroadcast = chatIsBroadcast
)
)
val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId)
val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id }
......
......@@ -8,11 +8,11 @@ import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import androidx.fragment.app.Fragment
import android.view.View
import android.view.ViewAnimationUtils
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.fragment.app.Fragment
fun View.rotateBy(value: Float, duration: Long = 100) {
animate()
......@@ -31,12 +31,12 @@ fun View.fadeIn(startValue: Float = 0f, finishValue: Float = 1f, duration: Long
.alpha(startValue)
.setDuration(duration / 2)
.setInterpolator(DecelerateInterpolator())
.withEndAction({
.withEndAction {
animate()
.alpha(finishValue)
.setDuration(duration / 2)
.setInterpolator(AccelerateInterpolator()).start()
}).start()
}.start()
setVisible(true)
}
......@@ -51,18 +51,25 @@ fun View.fadeOut(startValue: Float = 1f, finishValue: Float = 0f, duration: Long
.alpha(startValue)
.setDuration(duration)
.setInterpolator(DecelerateInterpolator())
.withEndAction({
.withEndAction {
animate()
.alpha(finishValue)
.setDuration(duration)
.setInterpolator(AccelerateInterpolator()).start()
}).start()
}.start()
setVisible(false)
}
fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float, endRadius: Float, duration: Long = 200) {
val anim = ViewAnimationUtils.createCircularReveal(this, centerX, centerY, startRadius, endRadius)
fun View.circularRevealOrUnreveal(
centerX: Int,
centerY: Int,
startRadius: Float,
endRadius: Float,
duration: Long = 200
) {
val anim =
ViewAnimationUtils.createCircularReveal(this, centerX, centerY, startRadius, endRadius)
anim.duration = duration
if (startRadius < endRadius) {
......@@ -74,7 +81,7 @@ fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float
anim.start()
}
fun View.shake(x: Float = 2F, num: Int = 0){
fun View.shake(x: Float = 2F, num: Int = 0) {
if (num == 6) {
this.translationX = 0.toFloat()
return
......@@ -104,5 +111,4 @@ fun Fragment.vibrateSmartPhone() {
} else {
vibrator.vibrate(200)
}
}
\ No newline at end of file
package chat.rocket.android.util.extensions
import android.graphics.Bitmap
fun Bitmap.getCompressFormat(mimeType: String): Bitmap.CompressFormat {
return when {
mimeType.contains("jpeg") -> Bitmap.CompressFormat.JPEG
mimeType.contains("webp") -> Bitmap.CompressFormat.WEBP
else -> Bitmap.CompressFormat.PNG
}
}
\ No newline at end of file
......@@ -2,17 +2,15 @@ package chat.rocket.android.util.extensions
import android.app.Activity
import android.content.Context
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import chat.rocket.android.R
// TODO: Remove. Use KTX instead.
......@@ -89,12 +87,3 @@ fun Fragment.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SH
fun Fragment.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) =
activity?.showToast(message, duration)
\ No newline at end of file
fun RecyclerView.isAtBottom(): Boolean {
val manager: RecyclerView.LayoutManager? = layoutManager
if (manager is LinearLayoutManager) {
return manager.findFirstVisibleItemPosition() == 0
}
return false // or true??? we can't determine the first visible item.
}
\ No newline at end of file
......@@ -3,6 +3,7 @@ package chat.rocket.android.util.extensions
import android.annotation.TargetApi
import android.content.ContentResolver
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
......@@ -57,25 +58,17 @@ fun Uri.getMimeType(context: Context): String {
}
}
fun Uri.getRealPathFromURI(context: Context): String? {
val cursor = context.contentResolver.query(this, arrayOf(MediaStore.Images.Media.DATA), null, null, null)
cursor.use { cursor ->
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
cursor.moveToFirst()
return cursor.getString(columnIndex)
}
}
@TargetApi(Build.VERSION_CODES.N)
fun Uri.isVirtualFile(context: Context): Boolean {
if (!DocumentsContract.isDocumentUri(context, this)) {
return false
}
val cursor = context.contentResolver.query(this,
val cursor = context.contentResolver.query(
this,
arrayOf(DocumentsContract.Document.COLUMN_FLAGS),
null, null, null)
null, null, null
)
var flags = 0
if (cursor.moveToFirst()) {
......@@ -97,8 +90,8 @@ fun Uri.getInputStreamForVirtualFile(context: Context, mimeTypeFilter: String):
throw FileNotFoundException()
}
return resolver.openTypedAssetFileDescriptor(this, openableMimeTypes[0],
null)?.createInputStream()
return resolver.openTypedAssetFileDescriptor(this, openableMimeTypes[0], null)
?.createInputStream()
}
fun Uri.getInputStream(context: Context): InputStream? {
......@@ -108,3 +101,7 @@ fun Uri.getInputStream(context: Context): InputStream? {
return context.contentResolver.openInputStream(this)
}
fun Uri.getBitmpap(context: Context): Bitmap? {
return MediaStore.Images.Media.getBitmap(context.contentResolver, this)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment