Unverified Commit 4f0e92d4 authored by Rafael Kellermann Streit's avatar Rafael Kellermann Streit Committed by GitHub

Merge branch 'develop' into emoji-skin-tone

parents 0951cb05 8c7899c3
...@@ -16,6 +16,12 @@ android { ...@@ -16,6 +16,12 @@ android {
versionName "2.4.0" versionName "2.4.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
} }
signingConfigs { signingConfigs {
...@@ -63,6 +69,8 @@ dependencies { ...@@ -63,6 +69,8 @@ dependencies {
implementation project(':player') implementation project(':player')
implementation project(':emoji') implementation project(':emoji')
implementation project(':draw') implementation project(':draw')
implementation project(':util')
implementation project(':core')
implementation libraries.kotlin implementation libraries.kotlin
implementation libraries.coroutines implementation libraries.coroutines
......
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "06359a8c2943365dd094bc5dff210203",
"entities": [
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `username` TEXT, `name` TEXT, `status` TEXT NOT NULL, `utcOffset` REAL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "utcOffset",
"columnName": "utcOffset",
"affinity": "REAL",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_users_username",
"unique": false,
"columnNames": [
"username"
],
"createSql": "CREATE INDEX `index_users_username` ON `${TABLE_NAME}` (`username`)"
}
],
"foreignKeys": []
},
{
"tableName": "chatrooms",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` TEXT NOT NULL, `type` TEXT NOT NULL, `name` TEXT NOT NULL, `fullname` TEXT, `userId` TEXT, `ownerId` TEXT, `readonly` INTEGER, `isDefault` INTEGER, `favorite` INTEGER, `open` INTEGER NOT NULL, `alert` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `userMentions` INTEGER, `groupMentions` INTEGER, `updatedAt` INTEGER, `timestamp` INTEGER, `lastSeen` INTEGER, `lastMessageText` TEXT, `lastMessageUserId` TEXT, `lastMessageTimestamp` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`ownerId`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`userId`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`lastMessageUserId`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "subscriptionId",
"columnName": "subscriptionId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fullname",
"columnName": "fullname",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "readonly",
"columnName": "readonly",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "isDefault",
"columnName": "isDefault",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "favorite",
"columnName": "favorite",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "open",
"columnName": "open",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alert",
"columnName": "alert",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userMentions",
"columnName": "userMentions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "groupMentions",
"columnName": "groupMentions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessageText",
"columnName": "lastMessageText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessageUserId",
"columnName": "lastMessageUserId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessageTimestamp",
"columnName": "lastMessageTimestamp",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_chatrooms_userId",
"unique": false,
"columnNames": [
"userId"
],
"createSql": "CREATE INDEX `index_chatrooms_userId` ON `${TABLE_NAME}` (`userId`)"
},
{
"name": "index_chatrooms_ownerId",
"unique": false,
"columnNames": [
"ownerId"
],
"createSql": "CREATE INDEX `index_chatrooms_ownerId` ON `${TABLE_NAME}` (`ownerId`)"
},
{
"name": "index_chatrooms_subscriptionId",
"unique": true,
"columnNames": [
"subscriptionId"
],
"createSql": "CREATE UNIQUE INDEX `index_chatrooms_subscriptionId` ON `${TABLE_NAME}` (`subscriptionId`)"
},
{
"name": "index_chatrooms_updatedAt",
"unique": false,
"columnNames": [
"updatedAt"
],
"createSql": "CREATE INDEX `index_chatrooms_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"ownerId"
],
"referencedColumns": [
"id"
]
},
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"userId"
],
"referencedColumns": [
"id"
]
},
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"lastMessageUserId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"06359a8c2943365dd094bc5dff210203\")"
]
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="chat.rocket.android"> package="chat.rocket.android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
...@@ -8,6 +9,7 @@ ...@@ -8,6 +9,7 @@
<application <application
android:name=".app.RocketChatApplication" android:name=".app.RocketChatApplication"
tools:replace="android:name"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_config" android:fullBackupContent="@xml/backup_config"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
......
...@@ -8,6 +8,7 @@ import chat.rocket.android.infrastructure.LocalRepository ...@@ -8,6 +8,7 @@ import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatAuthException import chat.rocket.common.RocketChatAuthException
...@@ -17,7 +18,6 @@ import chat.rocket.common.model.Token ...@@ -17,7 +18,6 @@ import chat.rocket.common.model.Token
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.* import chat.rocket.core.internal.rest.*
import com.google.android.gms.auth.api.credentials.Credential
import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.delay
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
......
...@@ -7,7 +7,7 @@ import chat.rocket.android.server.domain.* ...@@ -7,7 +7,7 @@ import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.registerPushToken import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.android.util.extensions.serverLogoUrl import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
......
...@@ -3,10 +3,9 @@ package chat.rocket.android.authentication.resetpassword.presentation ...@@ -3,10 +3,9 @@ package chat.rocket.android.authentication.resetpassword.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetConnectingServerInteractor import chat.rocket.android.server.domain.GetConnectingServerInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.isEmail import chat.rocket.android.util.extensions.isEmail
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatInvalidResponseException import chat.rocket.common.RocketChatInvalidResponseException
......
...@@ -10,16 +10,17 @@ import chat.rocket.android.server.domain.SaveConnectingServerInteractor ...@@ -10,16 +10,17 @@ import chat.rocket.android.server.domain.SaveConnectingServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extensions.isValidUrl import chat.rocket.android.util.extensions.isValidUrl
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import javax.inject.Inject import javax.inject.Inject
class ServerPresenter @Inject constructor(private val view: ServerView, class ServerPresenter @Inject constructor(
private val strategy: CancelStrategy, private val view: ServerView,
private val navigator: AuthenticationNavigator, private val strategy: CancelStrategy,
private val serverInteractor: SaveConnectingServerInteractor, private val navigator: AuthenticationNavigator,
private val refreshSettingsInteractor: RefreshSettingsInteractor, private val serverInteractor: SaveConnectingServerInteractor,
private val getAccountsInteractor: GetAccountsInteractor, private val refreshSettingsInteractor: RefreshSettingsInteractor,
factory: RocketChatClientFactory private val getAccountsInteractor: GetAccountsInteractor,
factory: RocketChatClientFactory
) : CheckServerPresenter(strategy, factory, view) { ) : CheckServerPresenter(strategy, factory, view) {
fun checkServer(server: String) { fun checkServer(server: String) {
......
...@@ -6,6 +6,7 @@ import chat.rocket.android.infrastructure.LocalRepository ...@@ -6,6 +6,7 @@ import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
......
...@@ -7,7 +7,7 @@ import chat.rocket.android.server.domain.* ...@@ -7,7 +7,7 @@ import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.registerPushToken import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.android.util.extensions.serverLogoUrl import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
......
...@@ -3,7 +3,6 @@ package chat.rocket.android.chatroom.domain ...@@ -3,7 +3,6 @@ package chat.rocket.android.chatroom.domain
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import java.io.File
import javax.inject.Inject import javax.inject.Inject
class UriInteractor @Inject constructor(private val context: Context) { class UriInteractor @Inject constructor(private val context: Context) {
...@@ -34,9 +33,4 @@ class UriInteractor @Inject constructor(private val context: Context) { ...@@ -34,9 +33,4 @@ class UriInteractor @Inject constructor(private val context: Context) {
* Note: It should be an image. * Note: It should be an image.
*/ */
fun getBitmap(uri: Uri) = uri.getBitmpap(context) fun getBitmap(uri: Uri) = uri.getBitmpap(context)
/**
* Returns the Uri from the [File].
*/
fun getUri(file: File) = Uri.fromFile(file)
} }
\ No newline at end of file
...@@ -50,14 +50,6 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -50,14 +50,6 @@ interface ChatRoomView : LoadingView, MessageView {
*/ */
fun showFileSelection(filter: Array<String>?) fun showFileSelection(filter: Array<String>?)
/**
* Uploads a file to a chat room.
*
* @param uri The file URI to send.
* @param msg Message to send with attachments
*/
fun uploadFile(uri: Uri, msg: String)
/** /**
* Shows a invalid file message. * Shows a invalid file message.
*/ */
...@@ -68,7 +60,7 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -68,7 +60,7 @@ interface ChatRoomView : LoadingView, MessageView {
* *
* @param message The (recent) message sent to a chat room. * @param message The (recent) message sent to a chat room.
*/ */
fun showNewMessage(message: List<BaseUiModel<*>>) fun showNewMessage(message: List<BaseUiModel<*>>, isMessageReceived: Boolean)
/** /**
* Dispatch to the recycler views adapter that we should remove a message. * Dispatch to the recycler views adapter that we should remove a message.
...@@ -119,7 +111,7 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -119,7 +111,7 @@ interface ChatRoomView : LoadingView, MessageView {
/** /**
* Clears the message composition. * Clears the message composition.
*/ */
fun clearMessageComposition() fun clearMessageComposition(deleteMessage: Boolean)
fun showInvalidFileSize(fileSize: Int, maxFileSize: Int) fun showInvalidFileSize(fileSize: Int, maxFileSize: Int)
......
package chat.rocket.android.chatroom.ui
import android.graphics.drawable.Drawable
import android.net.Uri
import androidx.core.view.isVisible
import chat.rocket.android.util.extensions.getFileName
import chat.rocket.android.util.extensions.getMimeType
fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
activity?.let { fragmentActivity ->
uri.getMimeType(fragmentActivity).let { mimeType ->
description.text.clear()
when {
mimeType.startsWith("image") -> {
imagePreview.isVisible = true
imagePreview.setImageURI(uri)
}
mimeType.startsWith("video") -> {
audioVideoAttachment.isVisible = true
}
else -> {
textFile.isVisible = true
textFile.text = uri.getFileName(fragmentActivity)
}
}
}
}
sendButton.setOnClickListener {
presenter.uploadFile(chatRoomId, uri, (citation ?: "") + description.text.toString())
alertDialog.dismiss()
}
cancelButton.setOnClickListener { alertDialog.dismiss() }
alertDialog.show()
}
fun ChatRoomFragment.showDrawAttachmentDialog(byteArray: ByteArray) {
description.text.clear()
imagePreview.isVisible = true
imagePreview.setImageDrawable(Drawable.createFromStream(byteArray.inputStream(), ""))
sendButton.setOnClickListener {
presenter.uploadDrawingImage(
chatRoomId,
byteArray,
(citation ?: "") + description.text.toString()
)
alertDialog.dismiss()
}
cancelButton.setOnClickListener { alertDialog.dismiss() }
alertDialog.show()
}
\ No newline at end of file
package chat.rocket.android.chatrooms.adapter
class LoadingItemHolder : ItemHolder<Unit> {
override val data: Unit
get() = Unit
}
\ No newline at end of file
package chat.rocket.android.chatrooms.adapter
import android.view.View
class LoadingViewHolder(itemView: View) : ViewHolder<ItemHolder<Unit>>(itemView) {
override fun bindViews(data: ItemHolder<Unit>) {
}
}
\ No newline at end of file
...@@ -18,8 +18,11 @@ import chat.rocket.android.util.extensions.avatarUrl ...@@ -18,8 +18,11 @@ import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.date import chat.rocket.android.util.extensions.date
import chat.rocket.android.util.extensions.localDateTime import chat.rocket.android.util.extensions.localDateTime
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.common.model.User
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.model.userStatusOf import chat.rocket.common.model.userStatusOf
import chat.rocket.core.model.Room
import chat.rocket.core.model.SpotlightResult
class RoomUiModelMapper( class RoomUiModelMapper(
private val context: Application, private val context: Application,
...@@ -34,7 +37,7 @@ class RoomUiModelMapper( ...@@ -34,7 +37,7 @@ class RoomUiModelMapper(
private val messageUnreadColor = ContextCompat.getColor(context, android.R.color.primary_text_light) private val messageUnreadColor = ContextCompat.getColor(context, android.R.color.primary_text_light)
private val messageColor = ContextCompat.getColor(context, R.color.colorSecondaryText) private val messageColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
fun map(rooms: List<ChatRoom>, grouped: Boolean): List<ItemHolder<*>> { fun map(rooms: List<ChatRoom>, grouped: Boolean = false): List<ItemHolder<*>> {
val list = ArrayList<ItemHolder<*>>(rooms.size + 4) val list = ArrayList<ItemHolder<*>>(rooms.size + 4)
var lastType: String? = null var lastType: String? = null
rooms.forEach { room -> rooms.forEach { room ->
...@@ -48,6 +51,47 @@ class RoomUiModelMapper( ...@@ -48,6 +51,47 @@ class RoomUiModelMapper(
return list return list
} }
fun map(spotlight: SpotlightResult): List<ItemHolder<*>> {
val list = ArrayList<ItemHolder<*>>(spotlight.users.size + spotlight.rooms.size)
spotlight.users.filterNot { it.username.isNullOrEmpty() }.forEach { user ->
list.add(RoomItemHolder(mapUser(user)))
}
spotlight.rooms.filterNot { it.name.isNullOrEmpty() }.forEach { room ->
list.add(RoomItemHolder(mapRoom(room)))
}
return list
}
private fun mapUser(user: User): RoomUiModel {
return with(user) {
val name = mapName(user.username!!, user.name, false)
val status = user.status
val avatar = serverUrl.avatarUrl(user.username!!)
RoomUiModel(
id = user.id,
name = name,
type = roomTypeOf(RoomType.DIRECT_MESSAGE),
avatar = avatar,
status = status
)
}
}
private fun mapRoom(room: Room): RoomUiModel {
return with(room) {
RoomUiModel(
id = id,
name = name!!,
type = type,
avatar = serverUrl.avatarUrl(name!!, isGroupOrChannel = true),
lastMessage = mapLastMessage(lastMessage?.sender?.username,
lastMessage?.sender?.name, lastMessage?.message)
)
}
}
fun map(chatRoom: ChatRoom): RoomUiModel { fun map(chatRoom: ChatRoom): RoomUiModel {
return with(chatRoom.chatRoom) { return with(chatRoom.chatRoom) {
val isUnread = alert || unread > 0 val isUnread = alert || unread > 0
...@@ -89,7 +133,7 @@ class RoomUiModelMapper( ...@@ -89,7 +133,7 @@ class RoomUiModelMapper(
} }
} }
private fun mapLastMessage(name: String?, fullName: String?, text: String?, unread: Boolean): CharSequence? { private fun mapLastMessage(name: String?, fullName: String?, text: String?, unread: Boolean = false): CharSequence? {
return if (!settings.showLastMessage()) { return if (!settings.showLastMessage()) {
null null
} else if (name != null && text != null) { } else if (name != null && text != null) {
......
...@@ -6,12 +6,13 @@ import android.view.View ...@@ -6,12 +6,13 @@ import android.view.View
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
import kotlinx.android.synthetic.main.item_chat.view.* import kotlinx.android.synthetic.main.item_chat.view.*
import kotlinx.android.synthetic.main.unread_messages_badge.view.* import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class RoomViewHolder(itemView: View, private val listener: (String) -> Unit) : ViewHolder<RoomItemHolder>(itemView) { class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit) : ViewHolder<RoomItemHolder>(itemView) {
private val resources: Resources = itemView.resources private val resources: Resources = itemView.resources
private val channelUnread: Drawable = resources.getDrawable(R.drawable.ic_hashtag_black_12dp) private val channelUnread: Drawable = resources.getDrawable(R.drawable.ic_hashtag_black_12dp)
...@@ -57,7 +58,7 @@ class RoomViewHolder(itemView: View, private val listener: (String) -> Unit) : V ...@@ -57,7 +58,7 @@ class RoomViewHolder(itemView: View, private val listener: (String) -> Unit) : V
} }
setOnClickListener { setOnClickListener {
listener(room.id) listener(room)
} }
} }
} }
......
...@@ -3,9 +3,10 @@ package chat.rocket.android.chatrooms.adapter ...@@ -3,9 +3,10 @@ package chat.rocket.android.chatrooms.adapter
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapter<ViewHolder<*>>() { class RoomsAdapter(private val listener: (RoomUiModel) -> Unit) : RecyclerView.Adapter<ViewHolder<*>>() {
init { init {
setHasStableIds(true) setHasStableIds(true)
...@@ -18,14 +19,17 @@ class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapte ...@@ -18,14 +19,17 @@ class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapte
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<*> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<*> {
if (viewType == 0) { if (viewType == VIEW_TYPE_ROOM) {
val view = parent.inflate(R.layout.item_chat) val view = parent.inflate(R.layout.item_chat)
return RoomViewHolder(view, listener) return RoomViewHolder(view, listener)
} else if (viewType == 1) { } else if (viewType == VIEW_TYPE_HEADER) {
val view = parent.inflate(R.layout.item_chatroom_header) val view = parent.inflate(R.layout.item_chatroom_header)
return HeaderViewHolder(view) return HeaderViewHolder(view)
} else if (viewType == VIEW_TYPE_LOADING) {
val view = parent.inflate(R.layout.item_loading)
return LoadingViewHolder(view)
} }
throw IllegalStateException("View type must be either Room or Header") throw IllegalStateException("View type must be either Room, Header or Loading")
} }
override fun getItemCount() = values.size override fun getItemCount() = values.size
...@@ -35,15 +39,17 @@ class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapte ...@@ -35,15 +39,17 @@ class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapte
return when(item) { return when(item) {
is HeaderItemHolder -> item.data.hashCode().toLong() is HeaderItemHolder -> item.data.hashCode().toLong()
is RoomItemHolder -> item.data.id.hashCode().toLong() is RoomItemHolder -> item.data.id.hashCode().toLong()
else -> throw IllegalStateException("View type must be either Room or Header") is LoadingItemHolder -> "loading".hashCode().toLong()
else -> throw IllegalStateException("View type must be either Room, Header or Loading")
} }
} }
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return when(values[position]) { return when(values[position]) {
is RoomItemHolder -> 0 is RoomItemHolder -> VIEW_TYPE_ROOM
is HeaderItemHolder -> 1 is HeaderItemHolder -> VIEW_TYPE_HEADER
else -> throw IllegalStateException("View type must be either Room or Header") is LoadingItemHolder -> VIEW_TYPE_LOADING
else -> throw IllegalStateException("View type must be either Room, Header or Loading")
} }
} }
...@@ -55,4 +61,9 @@ class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapte ...@@ -55,4 +61,9 @@ class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapte
} }
} }
companion object {
const val VIEW_TYPE_ROOM = 1
const val VIEW_TYPE_HEADER = 2
const val VIEW_TYPE_LOADING = 3
}
} }
\ No newline at end of file
...@@ -8,9 +8,9 @@ data class RoomUiModel( ...@@ -8,9 +8,9 @@ data class RoomUiModel(
val type: RoomType, val type: RoomType,
val name: CharSequence, val name: CharSequence,
val avatar: String, val avatar: String,
val date: CharSequence?, val date: CharSequence? = null,
val unread: String?, val unread: String? = null,
val alert: Boolean, val alert: Boolean = false,
val lastMessage: CharSequence?, val lastMessage: CharSequence? = null,
val status: UserStatus? val status: UserStatus? = null
) )
\ No newline at end of file
...@@ -71,7 +71,8 @@ class ChatRoomsFragmentModule { ...@@ -71,7 +71,8 @@ class ChatRoomsFragmentModule {
@Provides @Provides
@PerFragment @PerFragment
fun provideFetchChatRoomsInteractor(client: RocketChatClient, dbManager: DatabaseManager): FetchChatRoomsInteractor { fun provideFetchChatRoomsInteractor(client: RocketChatClient,
dbManager: DatabaseManager): FetchChatRoomsInteractor {
return FetchChatRoomsInteractor(client, dbManager) return FetchChatRoomsInteractor(client, dbManager)
} }
......
...@@ -3,13 +3,12 @@ package chat.rocket.android.chatrooms.domain ...@@ -3,13 +3,12 @@ package chat.rocket.android.chatrooms.domain
import chat.rocket.android.db.DatabaseManager import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.model.ChatRoomEntity import chat.rocket.android.db.model.ChatRoomEntity
import chat.rocket.android.db.model.UserEntity import chat.rocket.android.db.model.UserEntity
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.chatRooms import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.userId import chat.rocket.core.model.userId
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber import timber.log.Timber
class FetchChatRoomsInteractor( class FetchChatRoomsInteractor(
...@@ -18,21 +17,15 @@ class FetchChatRoomsInteractor( ...@@ -18,21 +17,15 @@ class FetchChatRoomsInteractor(
) { ) {
suspend fun refreshChatRooms() { suspend fun refreshChatRooms() {
launch(CommonPool) { val rooms = retryIO("fetch chatRooms", times = 10,
try { initialDelay = 200, maxDelay = 2000) {
val rooms = retryIO("fetch chatRooms", times = 10, client.chatRooms().update.map { room ->
initialDelay = 200, maxDelay = 2000) { mapChatRoom(room)
client.chatRooms().update.map { room ->
mapChatRoom(room)
}
}
Timber.d("Refreshing rooms: $rooms")
dbManager.insert(rooms)
} catch (ex: Exception) {
Timber.d(ex, "Error getting chatrooms")
} }
} }
Timber.d("Refreshing rooms: $rooms")
dbManager.insert(rooms)
} }
private suspend fun mapChatRoom(room: ChatRoom): ChatRoomEntity { private suspend fun mapChatRoom(room: ChatRoom): ChatRoomEntity {
......
...@@ -5,7 +5,7 @@ import chat.rocket.android.db.ChatRoomDao ...@@ -5,7 +5,7 @@ import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.model.ChatRoom import chat.rocket.android.db.model.ChatRoom
import javax.inject.Inject import javax.inject.Inject
class ChatRoomsRepository @Inject constructor(val dao: ChatRoomDao){ class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao){
fun getChatRooms(order: Order): LiveData<List<ChatRoom>> { fun getChatRooms(order: Order): LiveData<List<ChatRoom>> {
return when(order) { return when(order) {
Order.ACTIVITY -> dao.getAll() Order.ACTIVITY -> dao.getAll()
...@@ -15,6 +15,10 @@ class ChatRoomsRepository @Inject constructor(val dao: ChatRoomDao){ ...@@ -15,6 +15,10 @@ class ChatRoomsRepository @Inject constructor(val dao: ChatRoomDao){
} }
} }
fun search(query: String) = dao.searchSync(query)
fun count() = dao.count()
enum class Order { enum class Order {
ACTIVITY, ACTIVITY,
GROUPED_ACTIVITY, GROUPED_ACTIVITY,
......
package chat.rocket.android.chatrooms.presentation package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.model.ChatRoomEntity
import chat.rocket.android.helper.UserHelper import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.main.presentation.MainNavigator import chat.rocket.android.main.presentation.MainNavigator
...@@ -9,7 +12,7 @@ import chat.rocket.android.server.domain.SettingsRepository ...@@ -9,7 +12,7 @@ import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.useSpecialCharsOnRoom import chat.rocket.android.server.domain.useSpecialCharsOnRoom
import chat.rocket.android.server.domain.useRealName import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.infraestructure.ConnectionManager import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
...@@ -26,6 +29,7 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -26,6 +29,7 @@ class ChatRoomsPresenter @Inject constructor(
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: MainNavigator, private val navigator: MainNavigator,
@Named("currentServer") private val currentServer: String, @Named("currentServer") private val currentServer: String,
private val dbManager: DatabaseManager,
manager: ConnectionManager, manager: ConnectionManager,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val userHelper: UserHelper, private val userHelper: UserHelper,
...@@ -34,8 +38,33 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -34,8 +38,33 @@ class ChatRoomsPresenter @Inject constructor(
private val client = manager.client private val client = manager.client
private val settings = settingsRepository.get(currentServer) private val settings = settingsRepository.get(currentServer)
fun loadChatRoom(chatRoom: chat.rocket.android.db.model.ChatRoom) { fun loadChatRoom(chatRoom: RoomUiModel) {
with(chatRoom.chatRoom) { launchUI(strategy) {
view.showLoadingRoom(chatRoom.name)
try {
val room = dbManager.getRoom(chatRoom.id)
if (room != null) {
loadChatRoom(room.chatRoom)
} else {
with(chatRoom) {
val entity = ChatRoomEntity(
id = id,
subscriptionId = "",
type = type.toString(),
name = name.toString(),
open = false
)
loadChatRoom(entity)
}
}
} finally {
view.hideLoadingRoom()
}
}
}
fun loadChatRoom(chatRoom: ChatRoomEntity) {
with(chatRoom) {
val isDirectMessage = roomTypeOf(type) is RoomType.DirectMessage val isDirectMessage = roomTypeOf(type) is RoomType.DirectMessage
val roomName = if (settings.useSpecialCharsOnRoom() || (isDirectMessage && settings.useRealName())) { val roomName = if (settings.useSpecialCharsOnRoom() || (isDirectMessage && settings.useRealName())) {
fullname ?: name fullname ?: name
...@@ -61,7 +90,7 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -61,7 +90,7 @@ class ChatRoomsPresenter @Inject constructor(
} }
navigator.toChatRoom( navigator.toChatRoom(
chatRoomId = id, chatRoomId = id,
chatRoomName = roomName, chatRoomName = roomName,
chatRoomType = type, chatRoomType = type,
isReadOnly = readonly ?: false, isReadOnly = readonly ?: false,
......
...@@ -2,22 +2,9 @@ package chat.rocket.android.chatrooms.presentation ...@@ -2,22 +2,9 @@ package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
interface ChatRoomsView : LoadingView, MessageView { interface ChatRoomsView : LoadingView, MessageView {
fun showLoadingRoom(name: CharSequence)
/** fun hideLoadingRoom()
* Shows the chat rooms.
*
* @param newDataSet The new data set to show.
*/
suspend fun updateChatRooms(newDataSet: List<ChatRoom>)
/**
* Shows no chat rooms to display.
*/
fun showNoChatRoomsToDisplay()
fun showConnectionState(state: State)
} }
\ No newline at end of file
package chat.rocket.android.chatrooms.ui package chat.rocket.android.chatrooms.ui
import android.app.AlertDialog import android.app.AlertDialog
import android.app.ProgressDialog
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.view.LayoutInflater import android.view.LayoutInflater
...@@ -21,27 +22,25 @@ import androidx.recyclerview.widget.DefaultItemAnimator ...@@ -21,27 +22,25 @@ import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.RoomsAdapter import chat.rocket.android.chatrooms.adapter.RoomsAdapter
import chat.rocket.android.chatrooms.infrastructure.ChatRoomsRepository
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.chatrooms.viewmodel.ChatRoomsViewModel import chat.rocket.android.chatrooms.viewmodel.ChatRoomsViewModel
import chat.rocket.android.chatrooms.viewmodel.ChatRoomsViewModelFactory import chat.rocket.android.chatrooms.viewmodel.ChatRoomsViewModelFactory
import chat.rocket.android.chatrooms.viewmodel.LoadingState
import chat.rocket.android.chatrooms.viewmodel.Query
import chat.rocket.android.db.DatabaseManager import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.helper.ChatRoomsSortOrder import chat.rocket.android.helper.ChatRoomsSortOrder
import chat.rocket.android.helper.Constants import chat.rocket.android.helper.Constants
import chat.rocket.android.helper.SharedPreferenceHelper import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui import chat.rocket.android.util.extensions.ui
import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.widget.DividerItemDecoration import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_rooms.* import kotlinx.android.synthetic.main.fragment_chat_rooms.*
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
...@@ -58,9 +57,12 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -58,9 +57,12 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
lateinit var viewModel: ChatRoomsViewModel lateinit var viewModel: ChatRoomsViewModel
private var searchView: SearchView? = null private var searchView: SearchView? = null
private var sortView: MenuItem? = null
private val handler = Handler() private val handler = Handler()
private var chatRoomId: String? = null private var chatRoomId: String? = null
private var progressDialog: ProgressDialog? = null
companion object { companion object {
fun newInstance(chatRoomId: String? = null): ChatRoomsFragment { fun newInstance(chatRoomId: String? = null): ChatRoomsFragment {
...@@ -109,16 +111,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -109,16 +111,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
private fun subscribeUi() { private fun subscribeUi() {
ui { ui {
val adapter = RoomsAdapter { room ->
val adapter = RoomsAdapter { roomId -> presenter.loadChatRoom(room)
launch(UI) {
dbManager.getRoom(roomId)?.let { room ->
ui {
presenter.loadChatRoom(room)
}
}
}
} }
recycler_view.layoutManager = LinearLayoutManager(it) recycler_view.layoutManager = LinearLayoutManager(it)
...@@ -132,6 +126,23 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -132,6 +126,23 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
rooms?.let { rooms?.let {
Timber.d("Got items: $it") Timber.d("Got items: $it")
adapter.values = it adapter.values = it
if (rooms.isNotEmpty()) {
text_no_data_to_display.isVisible = false
}
}
})
viewModel.loadingState.observe(viewLifecycleOwner, Observer { state ->
when(state) {
is LoadingState.Loading -> if (state.count == 0L) showLoading()
is LoadingState.Loaded -> {
hideLoading()
if (state.count == 0L) showNoChatRoomsToDisplay()
}
is LoadingState.Error -> {
hideLoading()
showGenericErrorMessage()
}
} }
}) })
...@@ -147,6 +158,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -147,6 +158,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.chatrooms, menu) inflater.inflate(R.menu.chatrooms, menu)
sortView = menu.findItem(R.id.action_sort)
val searchItem = menu.findItem(R.id.action_search) val searchItem = menu.findItem(R.id.action_search)
searchView = searchItem?.actionView as? SearchView searchView = searchItem?.actionView as? SearchView
searchView?.setIconifiedByDefault(false) searchView?.setIconifiedByDefault(false)
...@@ -160,6 +173,21 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -160,6 +173,21 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
return queryChatRoomsByName(newText) return queryChatRoomsByName(newText)
} }
}) })
val expandListener = object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
// Simply setting sortView to visible won't work, so we invalidate the options
// to recreate the entire menu...
activity?.invalidateOptionsMenu()
return true
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
sortView?.isVisible = false
return true
}
}
searchItem?.setOnActionExpandListener(expandListener)
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
...@@ -209,25 +237,17 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -209,25 +237,17 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
val sortType = SharedPreferenceHelper.getInt(Constants.CHATROOM_SORT_TYPE_KEY, ChatRoomsSortOrder.ACTIVITY) val sortType = SharedPreferenceHelper.getInt(Constants.CHATROOM_SORT_TYPE_KEY, ChatRoomsSortOrder.ACTIVITY)
val grouped = SharedPreferenceHelper.getBoolean(Constants.CHATROOM_GROUP_BY_TYPE_KEY, false) val grouped = SharedPreferenceHelper.getBoolean(Constants.CHATROOM_GROUP_BY_TYPE_KEY, false)
val order = when(sortType) { val query = when(sortType) {
ChatRoomsSortOrder.ALPHABETICAL -> { ChatRoomsSortOrder.ALPHABETICAL -> {
if (grouped) { Query.ByName(grouped)
ChatRoomsRepository.Order.GROUPED_NAME
} else {
ChatRoomsRepository.Order.NAME
}
} }
ChatRoomsSortOrder.ACTIVITY -> { ChatRoomsSortOrder.ACTIVITY -> {
if (grouped) { Query.ByActivity(grouped)
ChatRoomsRepository.Order.GROUPED_ACTIVITY
} else {
ChatRoomsRepository.Order.ACTIVITY
}
} }
else -> ChatRoomsRepository.Order.ACTIVITY else -> Query.ByActivity()
} }
viewModel.setOrdering(order) viewModel.setQuery(query)
} }
private fun invalidateQueryOnSearch() { private fun invalidateQueryOnSearch() {
...@@ -238,20 +258,16 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -238,20 +258,16 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
} }
} }
override suspend fun updateChatRooms(newDataSet: List<ChatRoom>) {} private fun showNoChatRoomsToDisplay() {
override fun showNoChatRoomsToDisplay() {
ui { text_no_data_to_display.isVisible = true } ui { text_no_data_to_display.isVisible = true }
} }
override fun showLoading() { override fun showLoading() {
ui { view_loading.isVisible = true } view_loading.isVisible = true
} }
override fun hideLoading() { override fun hideLoading() {
ui { view_loading.isVisible = false
view_loading.isVisible = false
}
} }
override fun showMessage(resId: Int) { override fun showMessage(resId: Int) {
...@@ -268,7 +284,17 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -268,7 +284,17 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showConnectionState(state: State) { override fun showLoadingRoom(name: CharSequence) {
ui {
progressDialog = ProgressDialog.show(activity, "Rocket.Chat", "Loading room $name")
}
}
override fun hideLoadingRoom() {
progressDialog?.dismiss()
}
private fun showConnectionState(state: State) {
Timber.d("Got new state: $state") Timber.d("Got new state: $state")
ui { ui {
connection_status_text.fadeIn() connection_status_text.fadeIn()
...@@ -298,7 +324,11 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -298,7 +324,11 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
} }
private fun queryChatRoomsByName(name: String?): Boolean { private fun queryChatRoomsByName(name: String?): Boolean {
//presenter.chatRoomsByName(name ?: "") if (name.isNullOrEmpty()) {
updateSort()
} else {
viewModel.setQuery(Query.Search(name!!))
}
return true return true
} }
} }
\ No newline at end of file
...@@ -5,19 +5,30 @@ import androidx.lifecycle.MutableLiveData ...@@ -5,19 +5,30 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import chat.rocket.android.chatrooms.adapter.ItemHolder import chat.rocket.android.chatrooms.adapter.ItemHolder
import chat.rocket.android.chatrooms.adapter.LoadingItemHolder
import chat.rocket.android.chatrooms.adapter.RoomUiModelMapper import chat.rocket.android.chatrooms.adapter.RoomUiModelMapper
import chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor import chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor
import chat.rocket.android.chatrooms.infrastructure.ChatRoomsRepository import chat.rocket.android.chatrooms.infrastructure.ChatRoomsRepository
import chat.rocket.android.chatrooms.infrastructure.isGrouped
import chat.rocket.android.server.infraestructure.ConnectionManager import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.util.livedata.transform import chat.rocket.android.util.livedata.transform
import chat.rocket.android.util.livedata.wrap
import chat.rocket.android.util.retryIO
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.rest.spotlight
import chat.rocket.core.model.SpotlightResult
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.isActive
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.newSingleThreadContext import kotlinx.coroutines.experimental.newSingleThreadContext
import kotlinx.coroutines.experimental.withContext
import me.henrytao.livedataktx.distinct import me.henrytao.livedataktx.distinct
import me.henrytao.livedataktx.map import me.henrytao.livedataktx.map
import me.henrytao.livedataktx.nonNull import me.henrytao.livedataktx.nonNull
import timber.log.Timber import timber.log.Timber
import java.security.InvalidParameterException
import kotlin.coroutines.experimental.coroutineContext
class ChatRoomsViewModel( class ChatRoomsViewModel(
private val connectionManager: ConnectionManager, private val connectionManager: ConnectionManager,
...@@ -25,31 +36,65 @@ class ChatRoomsViewModel( ...@@ -25,31 +36,65 @@ class ChatRoomsViewModel(
private val repository: ChatRoomsRepository, private val repository: ChatRoomsRepository,
private val mapper: RoomUiModelMapper private val mapper: RoomUiModelMapper
) : ViewModel() { ) : ViewModel() {
private val ordering: MutableLiveData<ChatRoomsRepository.Order> = MutableLiveData() private val query = MutableLiveData<Query>()
val loadingState = MutableLiveData<LoadingState>()
private val runContext = newSingleThreadContext("chat-rooms-view-model") private val runContext = newSingleThreadContext("chat-rooms-view-model")
private val client = connectionManager.client
private var loaded = false
init { fun getChatRooms(): LiveData<RoomsModel> {
ordering.value = ChatRoomsRepository.Order.ACTIVITY return Transformations.switchMap(query) { query ->
} return@switchMap if (query.isSearch()) {
this@ChatRoomsViewModel.query.wrap(runContext) { _, data: MutableLiveData<RoomsModel> ->
val string = (query as Query.Search).query
fun getChatRooms(): LiveData<List<ItemHolder<*>>> { // debounce, to not query while the user is writing
return Transformations.switchMap(ordering) { order -> delay(200)
Timber.d("Querying rooms for order: $order") // TODO - find a better way for cancellation checking
repository.getChatRooms(order) if (!coroutineContext.isActive) return@wrap
.nonNull()
.distinct() val rooms = repository.search(string).let { mapper.map(it) }
.transform(runContext) { rooms -> data.postValue(rooms.toMutableList() + LoadingItemHolder())
rooms?.let { if (!coroutineContext.isActive) return@wrap
mapper.map(rooms, order.isGrouped())
} val spotlight = spotlight(query.query)?.let { mapper.map(it) }
if (!coroutineContext.isActive) return@wrap
spotlight?.let {
data.postValue(rooms.toMutableList() + spotlight)
}.ifNull {
data.postValue(rooms)
} }
}
} else {
repository.getChatRooms(query.asSortingOrder())
.nonNull()
.distinct()
.transform(runContext) { rooms ->
val mappedRooms = rooms?.let {
mapper.map(rooms, query.isGrouped())
}
if (loaded && mappedRooms?.isEmpty() == true) {
loadingState.postValue(LoadingState.Loaded(0))
}
mappedRooms
}
}
}
}
private suspend fun spotlight(query: String): SpotlightResult? {
return try {
retryIO { client.spotlight(query) }
} catch (ex: Exception) {
ex.printStackTrace()
null
} }
} }
fun getStatus(): MutableLiveData<State> { fun getStatus(): MutableLiveData<State> {
return connectionManager.statusLiveData.nonNull().distinct().map { state -> return connectionManager.statusLiveData.nonNull().distinct().map { state ->
if (state is State.Connected) { if (state is State.Connected) {
// TODO - add a loading status...
fetchRooms() fetchRooms()
} }
state state
...@@ -58,11 +103,69 @@ class ChatRoomsViewModel( ...@@ -58,11 +103,69 @@ class ChatRoomsViewModel(
private fun fetchRooms() { private fun fetchRooms() {
launch { launch {
interactor.refreshChatRooms() setLoadingState(LoadingState.Loading(repository.count()))
try {
interactor.refreshChatRooms()
setLoadingState(LoadingState.Loaded(repository.count()))
loaded = true
} catch (ex: Exception) {
Timber.d(ex, "Error refreshing chatrooms")
setLoadingState(LoadingState.Error(repository.count()))
}
}
}
fun setQuery(query: Query) {
this.query.value = query
}
private suspend fun setLoadingState(state: LoadingState) {
withContext(UI) {
loadingState.value = state
} }
} }
}
typealias RoomsModel = List<ItemHolder<*>>
sealed class LoadingState {
data class Loading(val count: Long) : LoadingState()
data class Loaded(val count: Long) : LoadingState()
data class Error(val count: Long) : LoadingState()
}
sealed class Query {
data class ByActivity(val grouped: Boolean = false) : Query()
data class ByName(val grouped: Boolean = false) : Query()
data class Search(val query: String) : Query()
}
fun Query.isSearch(): Boolean = this is Query.Search
fun setOrdering(order: ChatRoomsRepository.Order) { fun Query.isGrouped(): Boolean {
ordering.value = order return when(this) {
is Query.Search -> false
is Query.ByName -> grouped
is Query.ByActivity -> grouped
}
}
fun Query.asSortingOrder(): ChatRoomsRepository.Order {
return when(this) {
is Query.ByName -> {
if (grouped) {
ChatRoomsRepository.Order.GROUPED_NAME
} else {
ChatRoomsRepository.Order.NAME
}
}
is Query.ByActivity -> {
if (grouped) {
ChatRoomsRepository.Order.GROUPED_ACTIVITY
} else {
ChatRoomsRepository.Order.ACTIVITY
}
}
else -> throw InvalidParameterException("Should be ByName or ByActivity")
} }
} }
...@@ -5,7 +5,7 @@ import chat.rocket.android.main.presentation.MainNavigator ...@@ -5,7 +5,7 @@ import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.members.uimodel.MemberUiModelMapper import chat.rocket.android.members.uimodel.MemberUiModelMapper
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
......
...@@ -14,6 +14,8 @@ import chat.rocket.android.chatroom.ui.ChatRoomActivity ...@@ -14,6 +14,8 @@ import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider
import chat.rocket.android.createchannel.di.CreateChannelProvider import chat.rocket.android.createchannel.di.CreateChannelProvider
import chat.rocket.android.dagger.scope.PerActivity import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.draw.main.di.DrawModule
import chat.rocket.android.draw.main.ui.DrawingActivity
import chat.rocket.android.favoritemessages.di.FavoriteMessagesFragmentProvider import chat.rocket.android.favoritemessages.di.FavoriteMessagesFragmentProvider
import chat.rocket.android.files.di.FilesFragmentProvider import chat.rocket.android.files.di.FilesFragmentProvider
import chat.rocket.android.main.di.MainModule import chat.rocket.android.main.di.MainModule
...@@ -78,4 +80,8 @@ abstract class ActivityBuilder { ...@@ -78,4 +80,8 @@ abstract class ActivityBuilder {
@PerActivity @PerActivity
@ContributesAndroidInjector(modules = [ChangeServerModule::class]) @ContributesAndroidInjector(modules = [ChangeServerModule::class])
abstract fun bindChangeServerActivity(): ChangeServerActivity abstract fun bindChangeServerActivity(): ChangeServerActivity
@PerActivity
@ContributesAndroidInjector(modules = [DrawModule::class])
abstract fun bindDrawingActivity(): DrawingActivity
} }
\ No newline at end of file
...@@ -20,6 +20,17 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> { ...@@ -20,6 +20,17 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> {
""") """)
abstract fun get(id: String): ChatRoom? abstract fun get(id: String): ChatRoom?
@Transaction
@Query("$BASE_QUERY")
abstract fun getAllSync(): List<ChatRoom>
@Transaction
@Query("""$BASE_QUERY WHERE chatrooms.name LIKE '%' || :query || '%' OR users.name LIKE '%' || :query || '%'""")
abstract fun searchSync(query: String): List<ChatRoom>
@Query("SELECT COUNT(id) FROM chatrooms")
abstract fun count(): Long
@Transaction @Transaction
@Query(""" @Query("""
$BASE_QUERY $BASE_QUERY
...@@ -62,6 +73,15 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> { ...@@ -62,6 +73,15 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> {
@Query("DELETE FROM chatrooms WHERE ID = :id") @Query("DELETE FROM chatrooms WHERE ID = :id")
abstract fun delete(id: String) abstract fun delete(id: String)
@Query("DELETE FROM chatrooms")
abstract fun delete()
@Transaction
open fun cleanInsert(chatRooms: List<ChatRoomEntity>) {
delete()
insert(chatRooms)
}
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insertOrReplace(chatRooms: List<ChatRoomEntity>) abstract fun insertOrReplace(chatRooms: List<ChatRoomEntity>)
......
...@@ -35,6 +35,10 @@ class DatabaseManager(val context: Application, ...@@ -35,6 +35,10 @@ class DatabaseManager(val context: Application,
fun chatRoomDao(): ChatRoomDao = database.chatRoomDao() fun chatRoomDao(): ChatRoomDao = database.chatRoomDao()
fun userDao(): UserDao = database.userDao() fun userDao(): UserDao = database.userDao()
fun logout() {
database.clearAllTables()
}
suspend fun getRoom(id: String) = withContext(dbContext) { suspend fun getRoom(id: String) = withContext(dbContext) {
chatRoomDao().get(id) chatRoomDao().get(id)
} }
...@@ -297,7 +301,7 @@ class DatabaseManager(val context: Application, ...@@ -297,7 +301,7 @@ class DatabaseManager(val context: Application,
suspend fun insert(rooms: List<ChatRoomEntity>) { suspend fun insert(rooms: List<ChatRoomEntity>) {
withContext(dbContext) { withContext(dbContext) {
chatRoomDao().insert(rooms) chatRoomDao().cleanInsert(rooms)
} }
} }
......
...@@ -24,9 +24,9 @@ data class ChatRoomEntity( ...@@ -24,9 +24,9 @@ data class ChatRoomEntity(
var subscriptionId: String, var subscriptionId: String,
var type: String, var type: String,
var name: String, var name: String,
var fullname: String?, var fullname: String? = null,
var userId: String?, var userId: String? = null,
var ownerId: String?, var ownerId: String? = null,
var readonly: Boolean? = false, var readonly: Boolean? = false,
var isDefault: Boolean? = false, var isDefault: Boolean? = false,
var favorite: Boolean? = false, var favorite: Boolean? = false,
...@@ -38,9 +38,9 @@ data class ChatRoomEntity( ...@@ -38,9 +38,9 @@ data class ChatRoomEntity(
var updatedAt: Long? = -1, var updatedAt: Long? = -1,
var timestamp: Long? = -1, var timestamp: Long? = -1,
var lastSeen: Long? = -1, var lastSeen: Long? = -1,
var lastMessageText: String?, var lastMessageText: String? = null,
var lastMessageUserId: String?, var lastMessageUserId: String? = null,
var lastMessageTimestamp: Long? var lastMessageTimestamp: Long? = null
) )
data class ChatRoom( data class ChatRoom(
......
...@@ -4,7 +4,7 @@ import chat.rocket.android.chatroom.uimodel.UiModelMapper ...@@ -4,7 +4,7 @@ import chat.rocket.android.chatroom.uimodel.UiModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
......
...@@ -6,7 +6,7 @@ import chat.rocket.android.db.DatabaseManager ...@@ -6,7 +6,7 @@ import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.files.uimodel.FileUiModel import chat.rocket.android.files.uimodel.FileUiModel
import chat.rocket.android.files.uimodel.FileUiModelMapper import chat.rocket.android.files.uimodel.FileUiModelMapper
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
......
...@@ -84,7 +84,7 @@ object OauthHelper { ...@@ -84,7 +84,7 @@ object OauthHelper {
* @return The Facebook Oauth URL. * @return The Facebook Oauth URL.
*/ */
fun getFacebookOauthUrl(clientId: String, serverUrl: String, state: String): String { fun getFacebookOauthUrl(clientId: String, serverUrl: String, state: String): String {
return "https://facebook.com/v2.9/dialog/oauth" + return "https://facebook.com/v2.9/dialog/oauth" +
"?client_id=$clientId" + "?client_id=$clientId" +
"&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/facebook?close" + "&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/facebook?close" +
"&state=$state" + "&state=$state" +
...@@ -113,12 +113,18 @@ object OauthHelper { ...@@ -113,12 +113,18 @@ object OauthHelper {
state: String, state: String,
scope: String scope: String
): String { ): String {
return host + (authorizePath +
authorizePath +
"?client_id=$clientId" + "?client_id=$clientId" +
"&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/$serviceName" + "&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/$serviceName" +
"&state=$state" + "&state=$state" +
"&scope=$scope" + "&scope=$scope" +
"&response_type=code" "&response_type=code"
).let {
return if (it.contains(host)) {
it
} else {
host + it
}
}
} }
} }
\ No newline at end of file
...@@ -18,6 +18,8 @@ interface LocalRepository { ...@@ -18,6 +18,8 @@ interface LocalRepository {
fun clearAllFromServer(server: String) fun clearAllFromServer(server: String)
fun getCurrentUser(url: String): User? fun getCurrentUser(url: String): User?
fun saveCurrentUser(url: String, user: User) fun saveCurrentUser(url: String, user: User)
fun saveLastChatroomsRefresh(url: String, timestamp: Long)
fun getLastChatroomsRefresh(url: String): Long
companion object { companion object {
const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN" const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN"
...@@ -26,6 +28,7 @@ interface LocalRepository { ...@@ -26,6 +28,7 @@ interface LocalRepository {
const val PERMISSIONS_KEY = "permissions_" const val PERMISSIONS_KEY = "permissions_"
const val USER_KEY = "user_" const val USER_KEY = "user_"
const val CURRENT_USERNAME_KEY = "username_" const val CURRENT_USERNAME_KEY = "username_"
const val LAST_CHATROOMS_REFRESH = "_chatrooms_refresh"
} }
} }
......
...@@ -22,6 +22,13 @@ class SharedPreferencesLocalRepository( ...@@ -22,6 +22,13 @@ class SharedPreferencesLocalRepository(
save("${url}_${LocalRepository.USER_KEY}", userAdapter.toJson(user)) save("${url}_${LocalRepository.USER_KEY}", userAdapter.toJson(user))
} }
override fun saveLastChatroomsRefresh(url: String, timestamp: Long) {
save("$url${LocalRepository.LAST_CHATROOMS_REFRESH}", timestamp)
}
override fun getLastChatroomsRefresh(url: String) =
getLong("$url${LocalRepository.LAST_CHATROOMS_REFRESH}", 0L)
override fun getBoolean(key: String, defValue: Boolean) = preferences.getBoolean(key, defValue) override fun getBoolean(key: String, defValue: Boolean) = preferences.getBoolean(key, defValue)
override fun getFloat(key: String, defValue: Float) = preferences.getFloat(key, defValue) override fun getFloat(key: String, defValue: Float) = preferences.getFloat(key, defValue)
......
package chat.rocket.android.main.di package chat.rocket.android.main.di
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import android.content.Context
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.main.presentation.MainNavigator import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.main.presentation.MainView import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
...@@ -30,5 +28,6 @@ class MainModule { ...@@ -30,5 +28,6 @@ class MainModule {
fun provideLifecycleOwner(activity: MainActivity): LifecycleOwner = activity fun provideLifecycleOwner(activity: MainActivity): LifecycleOwner = activity
@Provides @Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy = CancelStrategy(owner, jobs) fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy =
CancelStrategy(owner, jobs)
} }
\ No newline at end of file
package chat.rocket.android.main.presentation package chat.rocket.android.main.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.main.uimodel.NavHeaderUiModel import chat.rocket.android.main.uimodel.NavHeaderUiModel
import chat.rocket.android.main.uimodel.NavHeaderUiModelMapper import chat.rocket.android.main.uimodel.NavHeaderUiModelMapper
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.RemoveAccountInteractor
import chat.rocket.android.server.domain.SaveAccountInteractor
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.favicon
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.registerPushToken import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.android.util.extensions.serverLogoUrl import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
...@@ -23,7 +31,9 @@ import chat.rocket.core.internal.rest.logout ...@@ -23,7 +31,9 @@ import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.me import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.unregisterPushToken import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.model.Myself import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
...@@ -39,11 +49,13 @@ class MainPresenter @Inject constructor( ...@@ -39,11 +49,13 @@ class MainPresenter @Inject constructor(
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInteractor: RemoveAccountInteractor, private val removeAccountInteractor: RemoveAccountInteractor,
private val factory: RocketChatClientFactory, private val factory: RocketChatClientFactory,
dbManagerFactory: DatabaseManagerFactory,
getSettingsInteractor: GetSettingsInteractor, getSettingsInteractor: GetSettingsInteractor,
managerFactory: ConnectionManagerFactory managerFactory: ConnectionManagerFactory
) : CheckServerPresenter(strategy, factory, view = view) { ) : CheckServerPresenter(strategy, factory, view = view) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val manager = managerFactory.create(currentServer) private val manager = managerFactory.create(currentServer)
private val dbManager = dbManagerFactory.create(currentServer)
private val client: RocketChatClient = factory.create(currentServer) private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!) private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
...@@ -91,6 +103,7 @@ class MainPresenter @Inject constructor( ...@@ -91,6 +103,7 @@ class MainPresenter @Inject constructor(
*/ */
fun logout() { fun logout() {
launchUI(strategy) { launchUI(strategy) {
view.showProgress()
try { try {
clearTokens() clearTokens()
retryIO("logout") { client.logout() } retryIO("logout") { client.logout() }
...@@ -107,10 +120,13 @@ class MainPresenter @Inject constructor( ...@@ -107,10 +120,13 @@ class MainPresenter @Inject constructor(
disconnect() disconnect()
removeAccountInteractor.remove(currentServer) removeAccountInteractor.remove(currentServer)
tokenRepository.remove(currentServer) tokenRepository.remove(currentServer)
withContext(CommonPool) { dbManager.logout() }
navigator.toNewServer() navigator.toNewServer()
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Error cleaning up the session...") Timber.d(ex, "Error cleaning up the session...")
} }
view.hideProgress()
} }
} }
......
...@@ -26,4 +26,7 @@ interface MainView : MessageView, VersionCheckView { ...@@ -26,4 +26,7 @@ interface MainView : MessageView, VersionCheckView {
fun closeServerSelection() fun closeServerSelection()
fun invalidateToken(token: String) fun invalidateToken(token: String)
fun showProgress()
fun hideProgress()
} }
\ No newline at end of file
...@@ -3,6 +3,7 @@ package chat.rocket.android.main.ui ...@@ -3,6 +3,7 @@ package chat.rocket.android.main.ui
import DrawableHelper import DrawableHelper
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.app.ProgressDialog
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
...@@ -269,4 +270,14 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, ...@@ -269,4 +270,14 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
fun setCheckedNavDrawerItem(@IdRes item: Int) { fun setCheckedNavDrawerItem(@IdRes item: Int) {
view_navigation.setCheckedItem(item) view_navigation.setCheckedItem(item)
} }
private var progressDialog : ProgressDialog? = null
override fun showProgress() {
progressDialog = ProgressDialog.show(this, getString(R.string.app_name), getString(R.string.msg_log_out), true, false)
}
override fun hideProgress() {
progressDialog?.dismiss()
progressDialog = null
}
} }
\ No newline at end of file
...@@ -5,7 +5,7 @@ import chat.rocket.android.db.DatabaseManager ...@@ -5,7 +5,7 @@ import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.members.uimodel.MemberUiModel import chat.rocket.android.members.uimodel.MemberUiModel
import chat.rocket.android.members.uimodel.MemberUiModelMapper import chat.rocket.android.members.uimodel.MemberUiModelMapper
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
......
...@@ -3,7 +3,7 @@ package chat.rocket.android.mentions.presentention ...@@ -3,7 +3,7 @@ package chat.rocket.android.mentions.presentention
import chat.rocket.android.chatroom.uimodel.UiModelMapper import chat.rocket.android.chatroom.uimodel.UiModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.getMentions import chat.rocket.core.internal.rest.getMentions
......
...@@ -4,7 +4,7 @@ import chat.rocket.android.chatroom.uimodel.UiModelMapper ...@@ -4,7 +4,7 @@ import chat.rocket.android.chatroom.uimodel.UiModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
......
...@@ -4,8 +4,8 @@ import chat.rocket.android.core.behaviours.showMessage ...@@ -4,8 +4,8 @@ import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
...@@ -15,10 +15,12 @@ import chat.rocket.core.internal.rest.setAvatar ...@@ -15,10 +15,12 @@ import chat.rocket.core.internal.rest.setAvatar
import chat.rocket.core.internal.rest.updateProfile import chat.rocket.core.internal.rest.updateProfile
import javax.inject.Inject import javax.inject.Inject
class ProfilePresenter @Inject constructor(private val view: ProfileView, class ProfilePresenter @Inject constructor(
private val strategy: CancelStrategy, private val view: ProfileView,
serverInteractor: GetCurrentServerInteractor, private val strategy: CancelStrategy,
factory: RocketChatClientFactory) { serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory
) {
private val serverUrl = serverInteractor.get()!! private val serverUrl = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(serverUrl) private val client: RocketChatClient = factory.create(serverUrl)
private lateinit var myselfId: String private lateinit var myselfId: String
...@@ -37,10 +39,10 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView, ...@@ -37,10 +39,10 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView,
val avatarUrl = serverUrl.avatarUrl(username) val avatarUrl = serverUrl.avatarUrl(username)
val email = myself.emails?.getOrNull(0)?.address val email = myself.emails?.getOrNull(0)?.address
view.showProfile( view.showProfile(
avatarUrl, avatarUrl,
myself.name ?: "", myself.name ?: "",
myself.username ?: "", myself.username ?: "",
email email
) )
} }
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
...@@ -55,11 +57,14 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView, ...@@ -55,11 +57,14 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView,
launchUI(strategy) { launchUI(strategy) {
view.showLoading() view.showLoading()
try { try {
if(avatarUrl!="") { if (avatarUrl != "") {
retryIO { client.setAvatar(avatarUrl) } retryIO { client.setAvatar(avatarUrl) }
} }
val user = retryIO { client.updateProfile( val user = retryIO {
userId = myselfId, email = email, name = name, username = username) } client.updateProfile(
userId = myselfId, email = email, name = name, username = username
)
}
view.showProfileUpdateSuccessfullyMessage() view.showProfileUpdateSuccessfullyMessage()
loadUserProfile() loadUserProfile()
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
......
...@@ -2,6 +2,7 @@ package chat.rocket.android.server.infraestructure ...@@ -2,6 +2,7 @@ package chat.rocket.android.server.infraestructure
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import chat.rocket.android.db.DatabaseManager import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.common.model.BaseRoom import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.User import chat.rocket.common.model.User
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
...@@ -40,10 +41,8 @@ class ConnectionManager( ...@@ -40,10 +41,8 @@ class ConnectionManager(
private val statusChannel = Channel<State>(Channel.CONFLATED) private val statusChannel = Channel<State>(Channel.CONFLATED)
private var connectJob: Job? = null private var connectJob: Job? = null
private val roomAndSubscriptionChannels = ArrayList<Channel<StreamMessage<BaseRoom>>>()
private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>() private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>()
private val userDataChannels = ArrayList<Channel<Myself>>() private val userDataChannels = ArrayList<Channel<Myself>>()
private val activeUsersChannels = ArrayList<Channel<User>>()
private val subscriptionIdMap = HashMap<String, String>() private val subscriptionIdMap = HashMap<String, String>()
private var subscriptionId: String? = null private var subscriptionId: String? = null
...@@ -126,9 +125,6 @@ class ConnectionManager( ...@@ -126,9 +125,6 @@ class ConnectionManager(
for (room in client.roomsChannel) { for (room in client.roomsChannel) {
Timber.d("GOT Room streamed") Timber.d("GOT Room streamed")
roomsActor.send(room) roomsActor.send(room)
for (channel in roomAndSubscriptionChannels) {
channel.send(room)
}
} }
} }
...@@ -137,9 +133,6 @@ class ConnectionManager( ...@@ -137,9 +133,6 @@ class ConnectionManager(
for (subscription in client.subscriptionsChannel) { for (subscription in client.subscriptionsChannel) {
Timber.d("GOT Subscription streamed") Timber.d("GOT Subscription streamed")
roomsActor.send(subscription) roomsActor.send(subscription)
for (channel in roomAndSubscriptionChannels) {
channel.send(subscription)
}
} }
} }
...@@ -170,9 +163,6 @@ class ConnectionManager( ...@@ -170,9 +163,6 @@ class ConnectionManager(
totalUsers++ totalUsers++
//Timber.d("Got activeUsers: $totalUsers") //Timber.d("Got activeUsers: $totalUsers")
userActor.send(user) userActor.send(user)
for (channel in activeUsersChannels) {
channel.send(user)
}
} }
} }
...@@ -205,20 +195,10 @@ class ConnectionManager( ...@@ -205,20 +195,10 @@ class ConnectionManager(
fun removeStatusChannel(channel: Channel<State>) = statusChannelList.remove(channel) fun removeStatusChannel(channel: Channel<State>) = statusChannelList.remove(channel)
fun addRoomsAndSubscriptionsChannel(channel: Channel<StreamMessage<BaseRoom>>) =
roomAndSubscriptionChannels.add(channel)
fun removeRoomsAndSubscriptionsChannel(channel: Channel<StreamMessage<BaseRoom>>) =
roomAndSubscriptionChannels.remove(channel)
fun addUserDataChannel(channel: Channel<Myself>) = userDataChannels.add(channel) fun addUserDataChannel(channel: Channel<Myself>) = userDataChannels.add(channel)
fun removeUserDataChannel(channel: Channel<Myself>) = userDataChannels.remove(channel) fun removeUserDataChannel(channel: Channel<Myself>) = userDataChannels.remove(channel)
fun addActiveUserChannel(channel: Channel<User>) = activeUsersChannels.add(channel)
fun removeActiveUserChannel(channel: Channel<User>) = activeUsersChannels.remove(channel)
fun subscribeRoomMessages(roomId: String, channel: Channel<Message>) { fun subscribeRoomMessages(roomId: String, channel: Channel<Message>) {
val oldSub = roomMessagesChannels.put(roomId, channel) val oldSub = roomMessagesChannels.put(roomId, channel)
if (oldSub != null) { if (oldSub != null) {
......
package chat.rocket.android.server.infraestructure package chat.rocket.android.server.infraestructure
import chat.rocket.android.db.DatabaseManagerFactory import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.infrastructure.LocalRepository
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
......
...@@ -4,7 +4,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy ...@@ -4,7 +4,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import javax.inject.Inject import javax.inject.Inject
......
...@@ -5,15 +5,13 @@ import chat.rocket.android.authentication.server.presentation.VersionCheckView ...@@ -5,15 +5,13 @@ import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.VersionInfo import chat.rocket.android.util.VersionInfo
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatInvalidProtocolException import chat.rocket.common.RocketChatInvalidProtocolException
import chat.rocket.common.model.ServerInfo import chat.rocket.common.model.ServerInfo
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.serverInfo import chat.rocket.core.internal.rest.serverInfo
import kotlinx.coroutines.experimental.Deferred
import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.async
import timber.log.Timber import timber.log.Timber
abstract class CheckServerPresenter constructor(private val strategy: CancelStrategy, abstract class CheckServerPresenter constructor(private val strategy: CancelStrategy,
...@@ -50,7 +48,7 @@ abstract class CheckServerPresenter constructor(private val strategy: CancelStra ...@@ -50,7 +48,7 @@ abstract class CheckServerPresenter constructor(private val strategy: CancelStra
} }
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Error getting server info") Timber.d(ex, "Error getting server info")
when(ex) { when (ex) {
is RocketChatInvalidProtocolException -> { is RocketChatInvalidProtocolException -> {
view.errorInvalidProtocol() view.errorInvalidProtocol()
} }
......
...@@ -3,7 +3,7 @@ package chat.rocket.android.settings.password.presentation ...@@ -3,7 +3,7 @@ package chat.rocket.android.settings.password.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
......
package chat.rocket.android.util package chat.rocket.android.util
import chat.rocket.android.BuildConfig
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType import okhttp3.MediaType
...@@ -28,6 +29,8 @@ class HttpLoggingInterceptor constructor(private val logger: Logger) : Intercept ...@@ -28,6 +29,8 @@ class HttpLoggingInterceptor constructor(private val logger: Logger) : Intercept
@Volatile @Volatile
internal var level = Level.NONE internal var level = Level.NONE
private val isDebug = BuildConfig.DEBUG
enum class Level { enum class Level {
/** No logs. */ /** No logs. */
NONE, NONE,
...@@ -140,7 +143,7 @@ class HttpLoggingInterceptor constructor(private val logger: Logger) : Intercept ...@@ -140,7 +143,7 @@ class HttpLoggingInterceptor constructor(private val logger: Logger) : Intercept
val name = headers.name(i) val name = headers.name(i)
// Skip headers from the request body as they are explicitly logged above. // Skip headers from the request body as they are explicitly logged above.
if (!"Content-Type".equals(name, ignoreCase = true) && !"Content-Length".equals(name, ignoreCase = true)) { if (!"Content-Type".equals(name, ignoreCase = true) && !"Content-Length".equals(name, ignoreCase = true)) {
if ("X-Auth-Token".equals(name, ignoreCase = true)) { if (!isDebug && "X-Auth-Token".equals(name, ignoreCase = true)) {
logger.log("$name: ${skipAuthToken(headers.value(i).length)}") logger.log("$name: ${skipAuthToken(headers.value(i).length)}")
} else { } else {
logger.log("$name: ${headers.value(i)}") logger.log("$name: ${headers.value(i)}")
......
package chat.rocket.android.util package chat.rocket.android.util
import chat.rocket.common.RocketChatNetworkErrorException import chat.rocket.common.RocketChatNetworkErrorException
import kotlinx.coroutines.experimental.TimeoutCancellationException
import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.isActive
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.experimental.coroutineContext
const val DEFAULT_RETRY = 3 const val DEFAULT_RETRY = 3
...@@ -16,14 +19,19 @@ suspend fun <T> retryIO( ...@@ -16,14 +19,19 @@ suspend fun <T> retryIO(
{ {
var currentDelay = initialDelay var currentDelay = initialDelay
repeat(times - 1) { currentTry -> repeat(times - 1) { currentTry ->
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
try { try {
return block() return block()
} catch (e: RocketChatNetworkErrorException) { } catch (e: RocketChatNetworkErrorException) {
Timber.d(e, "failed call($currentTry): $description") Timber.d(e, "failed call($currentTry): $description")
e.printStackTrace() e.printStackTrace()
} }
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
delay(currentDelay) delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
} }
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
return block() // last attempt return block() // last attempt
} }
\ 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
package chat.rocket.android.util.livedata
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.CoroutineScope
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import kotlin.coroutines.experimental.CoroutineContext
class WrappedLiveData<Source, Output>(
private val runContext: CoroutineContext = CommonPool,
private val source: LiveData<Source>,
private val transformation: suspend (Source?, MutableLiveData<Output>) -> Unit)
: MutableLiveData<Output>() {
private var job: Job? = null
private val observer = Observer<Source> { source ->
job?.cancel()
job = launch(runContext) {
transformation(source, this@WrappedLiveData)
}
}
override fun onActive() {
source.observeForever(observer)
}
override fun onInactive() {
job?.cancel()
source.removeObserver(observer)
}
}
fun <Source, Output> LiveData<Source>.wrap(
runContext: CoroutineContext = CommonPool,
transformation: suspend (Source?, MutableLiveData<Output>) -> Unit) =
WrappedLiveData(runContext, this, transformation)
\ No newline at end of file
...@@ -2,7 +2,9 @@ package chat.rocket.android.widget ...@@ -2,7 +2,9 @@ package chat.rocket.android.widget
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.View
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
...@@ -51,6 +53,9 @@ class DividerItemDecoration() : RecyclerView.ItemDecoration() { ...@@ -51,6 +53,9 @@ class DividerItemDecoration() : RecyclerView.ItemDecoration() {
for (i in 0 until childCount) { for (i in 0 until childCount) {
val child = parent.getChildAt(i) val child = parent.getChildAt(i)
if (isLastView(child, parent))
continue
val params = child.layoutParams as RecyclerView.LayoutParams val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin val top = child.bottom + params.bottomMargin
...@@ -60,4 +65,9 @@ class DividerItemDecoration() : RecyclerView.ItemDecoration() { ...@@ -60,4 +65,9 @@ class DividerItemDecoration() : RecyclerView.ItemDecoration() {
divider.draw(c) divider.draw(c)
} }
} }
private fun isLastView(view: View, parent: RecyclerView): Boolean {
val position = parent.getChildAdapterPosition(view)
return position == parent.adapter?.itemCount?.minus(1) ?: false
}
} }
\ No newline at end of file
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/colorAccent" />
<size
android:width="24dp"
android:height="24dp" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</FrameLayout>
\ No newline at end of file
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
...@@ -14,14 +15,29 @@ ...@@ -14,14 +15,29 @@
android:id="@+id/button_fab" android:id="@+id/button_fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="invisible"
android:layout_margin="16dp" android:layout_margin="16dp"
android:src="@drawable/ic_arrow_downward_24dp" android:src="@drawable/ic_arrow_downward_24dp"
android:theme="@style/Theme.AppCompat" android:theme="@style/Theme.AppCompat"
android:tint="@color/actionMenuColor" android:tint="@color/actionMenuColor"
android:visibility="invisible"
app:backgroundTint="@color/colorWhite" app:backgroundTint="@color/colorWhite"
app:fabSize="mini" tools:visibility="visible"
app:layout_anchor="@id/recycler_view" android:layout_gravity="end|bottom" />
app:layout_anchorGravity="bottom|end" />
<TextView
android:id="@+id/text_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="6dp"
android:gravity="center"
android:textColor="@color/colorWhite"
android:visibility="invisible"
android:textSize="@dimen/message_time_text_size"
android:background="@drawable/round_textview"
tools:visibility="visible"
tools:text="1"
android:layout_gravity="center_vertical"
app:layout_anchor="@id/button_fab"
app:layout_anchorGravity="end" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
...@@ -285,4 +285,5 @@ ...@@ -285,4 +285,5 @@
<string name="notif_action_reply_hint">RESPUESTA</string> <string name="notif_action_reply_hint">RESPUESTA</string>
<string name="notif_error_sending">La respuesta ha fallado. Inténtalo de nuevo.</string> <string name="notif_error_sending">La respuesta ha fallado. Inténtalo de nuevo.</string>
<string name="notif_success_sending">Mensaje enviado a %1$s!</string> <string name="notif_success_sending">Mensaje enviado a %1$s!</string>
<string name="msg_log_out">Saliendo de tu cuenta...</string>
</resources> </resources>
...@@ -287,4 +287,5 @@ ...@@ -287,4 +287,5 @@
<string name="notif_action_reply_hint">RÉPONDRE</string> <string name="notif_action_reply_hint">RÉPONDRE</string>
<string name="notif_error_sending">La réponse a échoué. Veuillez réessayer.</string> <string name="notif_error_sending">La réponse a échoué. Veuillez réessayer.</string>
<string name="notif_success_sending">Message envoyé à %1$s!</string> <string name="notif_success_sending">Message envoyé à %1$s!</string>
<string name="msg_log_out">Logging out...</string>
</resources> </resources>
...@@ -264,4 +264,5 @@ ...@@ -264,4 +264,5 @@
<string name="notif_action_reply_hint">जवाब</string> <string name="notif_action_reply_hint">जवाब</string>
<string name="notif_error_sending">उत्तर विफल हुआ है। कृपया फिर से प्रयास करें।</string> <string name="notif_error_sending">उत्तर विफल हुआ है। कृपया फिर से प्रयास करें।</string>
<string name="notif_success_sending">संदेश भेजा गया %1$s!</string> <string name="notif_success_sending">संदेश भेजा गया %1$s!</string>
<string name="msg_log_out">Logging out...</string>
</resources> </resources>
\ No newline at end of file
...@@ -266,4 +266,5 @@ ...@@ -266,4 +266,5 @@
<string name="notif_action_reply_hint">RESPONDER</string> <string name="notif_action_reply_hint">RESPONDER</string>
<string name="notif_error_sending">Falha ao enviar a mensagem.</string> <string name="notif_error_sending">Falha ao enviar a mensagem.</string>
<string name="notif_success_sending">Mensagem enviada para %1$s!</string> <string name="notif_success_sending">Mensagem enviada para %1$s!</string>
<string name="msg_log_out">Deslogando...</string>
</resources> </resources>
...@@ -261,4 +261,5 @@ ...@@ -261,4 +261,5 @@
<string name="notif_action_reply_hint">ОТВЕТИТЬ</string> <string name="notif_action_reply_hint">ОТВЕТИТЬ</string>
<string name="notif_error_sending">Ошибка ответа. Пожалуйста, попробуйте еще раз.</string> <string name="notif_error_sending">Ошибка ответа. Пожалуйста, попробуйте еще раз.</string>
<string name="notif_success_sending">Сообщение отправлено %1$s!</string> <string name="notif_success_sending">Сообщение отправлено %1$s!</string>
<string name="msg_log_out">Logging out...</string>
</resources> </resources>
...@@ -120,6 +120,7 @@ ...@@ -120,6 +120,7 @@
<string name="msg_are_typing">\u0020are typing…</string> <string name="msg_are_typing">\u0020are typing…</string>
<string name="msg_several_users_are_typing">Several users are typing…</string> <string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">No result found</string> <string name="msg_no_search_found">No result found</string>
<string name="msg_log_out">Logging out...</string>
<string name="msg_upload_file">Upload file</string> <string name="msg_upload_file">Upload file</string>
<string name="msg_file_description">File description</string> <string name="msg_file_description">File description</string>
<string name="msg_send">Send</string> <string name="msg_send">Send</string>
......
...@@ -5,8 +5,8 @@ buildscript { ...@@ -5,8 +5,8 @@ buildscript {
repositories { repositories {
google() google()
jcenter() jcenter()
maven { url 'https://maven.fabric.io/public' }
mavenCentral() mavenCentral()
maven { url 'https://maven.fabric.io/public' }
} }
dependencies { dependencies {
......
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion versions.compileSdk
defaultConfig {
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
versionCode 1
versionName "1.0.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation libraries.kotlin
implementation libraries.coroutines
implementation libraries.lifecycleExtensions
kapt libraries.lifecycleCompiler
}
\ No newline at end of file
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.rocket.android.core" />
...@@ -5,9 +5,8 @@ import androidx.lifecycle.LifecycleObserver ...@@ -5,9 +5,8 @@ import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.Job
import javax.inject.Inject
class CancelStrategy @Inject constructor(owner: LifecycleOwner, val jobs: Job) : LifecycleObserver { class CancelStrategy(owner: LifecycleOwner, val jobs: Job) : LifecycleObserver {
init { init {
owner.lifecycle.addObserver(this) owner.lifecycle.addObserver(this)
......
<resources>
<string name="app_name">Core</string>
</resources>
...@@ -5,7 +5,7 @@ ext { ...@@ -5,7 +5,7 @@ ext {
targetSdk : 28, targetSdk : 28,
minSdk : 21, minSdk : 21,
buildTools : '28.0.0-rc2', buildTools : '28.0.0-rc2',
kotlin : '1.2.50', kotlin : '1.2.51',
coroutine : '0.23.1', coroutine : '0.23.1',
dokka : '0.9.16', dokka : '0.9.16',
...@@ -22,7 +22,7 @@ ext { ...@@ -22,7 +22,7 @@ ext {
playServices : '15.0.0', playServices : '15.0.0',
firebase : '15.0.0', firebase : '15.0.0',
room : '2.0.0-alpha1', room : '2.0.0-alpha1',
lifecycle : '2.0.0-alpha1', lifecycle : '2.0.0-beta01',
rxKotlin : '2.2.0', rxKotlin : '2.2.0',
rxAndroid : '2.0.2', rxAndroid : '2.0.2',
moshi : '1.6.0', moshi : '1.6.0',
......
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion versions.compileSdk compileSdkVersion versions.compileSdk
buildToolsVersion versions.buildTools buildToolsVersion versions.buildTools
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
...@@ -18,21 +20,24 @@ android { ...@@ -18,21 +20,24 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation libraries.appCompat implementation project(':util')
implementation project(':core')
implementation libraries.kotlin implementation libraries.kotlin
implementation libraries.coroutines
implementation libraries.appCompat
implementation libraries.constraintlayout implementation libraries.constraintlayout
implementation libraries.androidKtx
testImplementation 'junit:junit:4.12' implementation libraries.androidKtx
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
apply plugin: 'kotlin-android' implementation libraries.dagger
apply plugin: 'kotlin-android-extensions' implementation libraries.daggerSupport
\ No newline at end of file kapt libraries.daggerProcessor
kapt libraries.daggerAndroidApt
}
\ No newline at end of file
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.rocket.android.draw"> package="chat.rocket.android.draw">
<application> <application android:name=".DrawApplication">
<activity <activity
android:name=".DrawingActivity" android:name=".main.ui.DrawingActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" /> android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
</application> </application>
......
package chat.rocket.android.draw
import chat.rocket.android.draw.dagger.DaggerAppComponent
import dagger.android.AndroidInjector
import dagger.android.support.DaggerApplication
class DrawApplication : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> =
DaggerAppComponent.builder().create(this)
}
\ No newline at end of file
package chat.rocket.android.draw.dagger
import chat.rocket.android.draw.dagger.module.ActivityBuilderModule
import dagger.Component
import dagger.android.AndroidInjector
import dagger.android.support.AndroidSupportInjectionModule
import dagger.android.support.DaggerApplication
@Component(modules = [AndroidSupportInjectionModule::class, ActivityBuilderModule::class])
interface AppComponent : AndroidInjector<DaggerApplication> {
@Component.Builder
abstract class Builder : AndroidInjector.Builder<DaggerApplication>()
}
\ No newline at end of file
package chat.rocket.android.draw.dagger.module
import chat.rocket.android.draw.main.di.DrawModule
import chat.rocket.android.draw.main.ui.DrawingActivity
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class ActivityBuilderModule {
@ContributesAndroidInjector(modules = [DrawModule::class])
abstract fun contributeDrawingActivityInjector(): DrawingActivity
}
\ No newline at end of file
package chat.rocket.android.draw.main.di
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.draw.main.presenter.DrawView
import chat.rocket.android.draw.main.ui.DrawingActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
class DrawModule {
@Provides
fun provideMainView(activity: DrawingActivity): DrawView = activity
@Provides
fun provideJob() = Job()
@Provides
fun provideLifecycleOwner(activity: DrawingActivity): LifecycleOwner = activity
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy =
CancelStrategy(owner, jobs)
}
\ No newline at end of file
package chat.rocket.android.draw.main.presenter
import android.graphics.Bitmap
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.util.extension.compressImageAndGetByteArray
import chat.rocket.android.util.extension.launchUI
import javax.inject.Inject
class DrawPresenter @Inject constructor(
private val view: DrawView,
private val strategy: CancelStrategy
) {
fun processDrawingImage(bitmap: Bitmap) {
launchUI(strategy) {
val byteArray = bitmap.compressImageAndGetByteArray("image/png")
if (byteArray != null) {
view.sendByteArray(byteArray)
} else {
view.showWrongProcessingMessage()
}
}
}
}
\ No newline at end of file
package chat.rocket.android.draw.main.presenter
interface DrawView {
/**
* Sends the [ByteArray] of the processed draw image (compressed).
*/
fun sendByteArray(byteArray: ByteArray)
/**
* Shows a message indicating that something was wrong while processing the draw image.
*/
fun showWrongProcessingMessage()
}
\ No newline at end of file
package chat.rocket.android.draw package chat.rocket.android.draw.main.ui
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.SeekBar import android.widget.SeekBar
import androidx.appcompat.app.AppCompatActivity import android.widget.Toast
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import chat.rocket.android.draw.R
import chat.rocket.android.draw.main.presenter.DrawPresenter
import chat.rocket.android.draw.main.presenter.DrawView
import dagger.android.support.DaggerAppCompatActivity
import kotlinx.android.synthetic.main.activity_drawing.* import kotlinx.android.synthetic.main.activity_drawing.*
import kotlinx.android.synthetic.main.color_palette_view.* import kotlinx.android.synthetic.main.color_palette_view.*
import java.io.ByteArrayOutputStream import javax.inject.Inject
class DrawingActivity : AppCompatActivity() { const val DRAWING_BYTE_ARRAY_EXTRA_DATA: String = "chat.rocket.android.DrawingByteArray"
class DrawingActivity : DaggerAppCompatActivity(), DrawView {
@Inject
lateinit var presenter: DrawPresenter
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_drawing) setContentView(R.layout.activity_drawing)
image_close_drawing.setOnClickListener { setupListeners()
finish() setupDrawTools()
} colorSelector()
image_send_drawing.setOnClickListener { setPaintAlpha()
val bStream = ByteArrayOutputStream() setPaintWidth()
val bitmap = custom_draw_view.getBitmap() }
bitmap.compress(Bitmap.CompressFormat.PNG, 70, bStream)
val byteArray = bStream.toByteArray()
val returnIntent = Intent()
returnIntent.putExtra("bitmap", byteArray)
setResult(Activity.RESULT_OK,returnIntent)
finish()
}
setUpDrawTools() override fun sendByteArray(byteArray: ByteArray) {
setResult(Activity.RESULT_OK, Intent().putExtra(DRAWING_BYTE_ARRAY_EXTRA_DATA, byteArray))
finish()
}
colorSelector() override fun showWrongProcessingMessage() {
Toast.makeText(this, getText(R.string.msg_wrong_processing_draw_image), Toast.LENGTH_SHORT)
.show()
}
setPaintAlpha() private fun setupListeners() {
image_close_drawing.setOnClickListener { finish() }
setPaintWidth() image_send_drawing.setOnClickListener {
presenter.processDrawingImage(custom_draw_view.getBitmap())
}
} }
private fun setUpDrawTools() { private fun setupDrawTools() {
image_draw_eraser.setOnClickListener { image_draw_eraser.setOnClickListener {
custom_draw_view.clearCanvas() custom_draw_view.clearCanvas()
toggleDrawTools(draw_tools,false) toggleDrawTools(draw_tools, false)
} }
image_draw_width.setOnClickListener { image_draw_width.setOnClickListener {
if (draw_tools.translationY == (56).toPx){ if (draw_tools.translationY == (56).toPx) {
toggleDrawTools(draw_tools,true) toggleDrawTools(draw_tools, true)
}else if (draw_tools.translationY == (0).toPx && seekBar_width.isVisible){ } else if (draw_tools.translationY == (0).toPx && seekBar_width.isVisible) {
toggleDrawTools(draw_tools,false) toggleDrawTools(draw_tools, false)
} }
seekBar_width.isVisible = true seekBar_width.isVisible = true
seekBar_opacity.isVisible = false seekBar_opacity.isVisible = false
draw_color_palette.isVisible = false draw_color_palette.isVisible = false
} }
image_draw_opacity.setOnClickListener { image_draw_opacity.setOnClickListener {
if (draw_tools.translationY == (56).toPx){ if (draw_tools.translationY == (56).toPx) {
toggleDrawTools(draw_tools,true) toggleDrawTools(draw_tools, true)
}else if (draw_tools.translationY == (0).toPx && seekBar_opacity.isVisible){ } else if (draw_tools.translationY == (0).toPx && seekBar_opacity.isVisible) {
toggleDrawTools(draw_tools,false) toggleDrawTools(draw_tools, false)
} }
seekBar_width.isVisible = false seekBar_width.isVisible = false
seekBar_opacity.isVisible = true seekBar_opacity.isVisible = true
draw_color_palette.isVisible = false draw_color_palette.isVisible = false
} }
image_draw_color.setOnClickListener { image_draw_color.setOnClickListener {
if (draw_tools.translationY == (56).toPx){ if (draw_tools.translationY == (56).toPx) {
toggleDrawTools(draw_tools,true) toggleDrawTools(draw_tools, true)
}else if (draw_tools.translationY == (0).toPx && draw_color_palette.isVisible){ } else if (draw_tools.translationY == (0).toPx && draw_color_palette.isVisible) {
toggleDrawTools(draw_tools,false) toggleDrawTools(draw_tools, false)
} }
seekBar_width.isVisible = false seekBar_width.isVisible = false
seekBar_opacity.isVisible = false seekBar_opacity.isVisible = false
draw_color_palette.isVisible = true draw_color_palette.isVisible = true
} }
image_draw_undo.setOnClickListener { image_draw_undo.setOnClickListener {
custom_draw_view.undo() custom_draw_view.undo()
toggleDrawTools(draw_tools,false) toggleDrawTools(draw_tools, false)
} }
image_draw_redo.setOnClickListener { image_draw_redo.setOnClickListener {
custom_draw_view.redo() custom_draw_view.redo()
toggleDrawTools(draw_tools,false) toggleDrawTools(draw_tools, false)
} }
} }
private fun toggleDrawTools(view: View, showView: Boolean = true) { private fun toggleDrawTools(view: View, showView: Boolean = true) {
if (showView){ if (showView) {
view.animate().translationY((0).toPx) view.animate().translationY((0).toPx)
}else{ } else {
view.animate().translationY((56).toPx) view.animate().translationY((56).toPx)
} }
} }
private fun colorSelector() { private fun colorSelector() {
image_color_black.setOnClickListener {
custom_draw_view.setColor(ResourcesCompat.getColor(resources, R.color.color_black,null))
scaleColorView(image_color_black)
}
image_color_red.setOnClickListener { image_color_red.setOnClickListener {
custom_draw_view.setColor(ResourcesCompat.getColor(resources, R.color.color_red,null)) custom_draw_view.setColor(
ResourcesCompat.getColor(resources, R.color.color_red, null)
)
scaleColorView(image_color_red) scaleColorView(image_color_red)
} }
image_color_yellow.setOnClickListener { image_color_yellow.setOnClickListener {
custom_draw_view.setColor(ResourcesCompat.getColor(resources, R.color.color_yellow,null)) custom_draw_view.setColor(
ResourcesCompat.getColor(
resources,
R.color.color_yellow, null
)
)
scaleColorView(image_color_yellow) scaleColorView(image_color_yellow)
} }
image_color_green.setOnClickListener { image_color_green.setOnClickListener {
custom_draw_view.setColor(ResourcesCompat.getColor(resources, R.color.color_green,null)) custom_draw_view.setColor(
ResourcesCompat.getColor(
resources,
R.color.color_green, null
)
)
scaleColorView(image_color_green) scaleColorView(image_color_green)
} }
image_color_blue.setOnClickListener { image_color_blue.setOnClickListener {
custom_draw_view.setColor(ResourcesCompat.getColor(resources, R.color.color_blue,null)) custom_draw_view.setColor(
ResourcesCompat.getColor(resources, R.color.color_blue, null)
)
scaleColorView(image_color_blue) scaleColorView(image_color_blue)
} }
image_color_pink.setOnClickListener { image_color_pink.setOnClickListener {
custom_draw_view.setColor(ResourcesCompat.getColor(resources, R.color.color_pink,null)) custom_draw_view.setColor(
ResourcesCompat.getColor(
resources,
R.color.color_pink, null
)
)
scaleColorView(image_color_pink) scaleColorView(image_color_pink)
} }
image_color_brown.setOnClickListener { image_color_brown.setOnClickListener {
custom_draw_view.setColor(ResourcesCompat.getColor(resources, R.color.color_brown,null)) custom_draw_view.setColor(
ResourcesCompat.getColor(
resources,
R.color.color_brown, null
)
)
scaleColorView(image_color_brown) scaleColorView(image_color_brown)
} }
} }
...@@ -156,7 +195,7 @@ class DrawingActivity : AppCompatActivity() { ...@@ -156,7 +195,7 @@ class DrawingActivity : AppCompatActivity() {
} }
private fun setPaintWidth() { private fun setPaintWidth() {
seekBar_width.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener{ seekBar_width.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
custom_draw_view.setStrokeWidth(progress.toFloat()) custom_draw_view.setStrokeWidth(progress.toFloat())
} }
...@@ -168,7 +207,7 @@ class DrawingActivity : AppCompatActivity() { ...@@ -168,7 +207,7 @@ class DrawingActivity : AppCompatActivity() {
} }
private fun setPaintAlpha() { private fun setPaintAlpha() {
seekBar_opacity.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener{ seekBar_opacity.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
custom_draw_view.setAlpha(progress) custom_draw_view.setAlpha(progress)
} }
...@@ -181,4 +220,4 @@ class DrawingActivity : AppCompatActivity() { ...@@ -181,4 +220,4 @@ class DrawingActivity : AppCompatActivity() {
private val Int.toPx: Float private val Int.toPx: Float
get() = (this * Resources.getSystem().displayMetrics.density) get() = (this * Resources.getSystem().displayMetrics.density)
} }
\ No newline at end of file
...@@ -6,5 +6,4 @@ import java.io.Serializable ...@@ -6,5 +6,4 @@ import java.io.Serializable
interface Action : Serializable { interface Action : Serializable {
fun perform(path: Path) fun perform(path: Path)
}
} \ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".DrawingActivity"> tools:context=".ui.DrawingActivity">
<chat.rocket.android.draw.widget.CustomDrawView <chat.rocket.android.draw.widget.CustomDrawView
android:id="@+id/custom_draw_view" android:id="@+id/custom_draw_view"
...@@ -17,143 +16,143 @@ ...@@ -17,143 +16,143 @@
android:id="@+id/image_close_drawing" android:id="@+id/image_close_drawing"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"
android:background="@color/color_white"
android:foreground="?selectableItemBackgroundBorderless"
android:padding="16dp"
android:src="@drawable/ic_close_black_24dp" android:src="@drawable/ic_close_black_24dp"
android:tint="@color/icon_color" android:tint="@color/icon_color"
android:padding="16dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent" />
android:background="@color/color_white"
android:foreground="?selectableItemBackgroundBorderless" />
<ImageView <ImageView
android:id="@+id/image_send_drawing" android:id="@+id/image_send_drawing"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"
android:src="@drawable/ic_send_black_24dp" android:background="@color/color_white"
android:foreground="?selectableItemBackgroundBorderless"
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_send_black_24dp"
android:tint="@color/colorAccent" android:tint="@color/colorAccent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent" />
android:background="@color/color_white"
android:foreground="?selectableItemBackgroundBorderless" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/draw_tools" android:id="@+id/draw_tools"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:background="@color/color_white" android:background="@color/color_white"
android:elevation="4dp" android:elevation="4dp"
android:translationY="56dp" > android:translationY="56dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<ImageView <ImageView
android:id="@+id/image_draw_eraser" android:id="@+id/image_draw_eraser"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"
android:src="@drawable/ic_eraser_black_24dp" android:foreground="?selectableItemBackground"
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_eraser_black_24dp"
android:tint="@color/icon_color" android:tint="@color/icon_color"
android:foreground="?selectableItemBackground" app:layout_constraintEnd_toStartOf="@id/image_draw_width"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/image_draw_width" app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"/>
<ImageView <ImageView
android:id="@+id/image_draw_width" android:id="@+id/image_draw_width"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"
android:src="@drawable/ic_adjust_black_24dp" android:foreground="?selectableItemBackground"
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_adjust_black_24dp"
android:tint="@color/icon_color" android:tint="@color/icon_color"
android:foreground="?selectableItemBackground"
app:layout_constraintStart_toEndOf="@id/image_draw_eraser"
app:layout_constraintEnd_toStartOf="@id/image_draw_color" app:layout_constraintEnd_toStartOf="@id/image_draw_color"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintStart_toEndOf="@id/image_draw_eraser"
app:layout_constraintTop_toTopOf="parent" />
<ImageView <ImageView
android:id="@+id/image_draw_color" android:id="@+id/image_draw_color"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"
android:src="@drawable/ic_color_lens_black_24dp" android:foreground="?selectableItemBackground"
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_color_lens_black_24dp"
android:tint="@color/icon_color" android:tint="@color/icon_color"
android:foreground="?selectableItemBackground"
app:layout_constraintStart_toEndOf="@id/image_draw_width"
app:layout_constraintEnd_toStartOf="@id/image_draw_opacity" app:layout_constraintEnd_toStartOf="@id/image_draw_opacity"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintStart_toEndOf="@id/image_draw_width"
app:layout_constraintTop_toTopOf="parent" />
<ImageView <ImageView
android:id="@+id/image_draw_opacity" android:id="@+id/image_draw_opacity"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"
android:src="@drawable/ic_opacity_black_24dp" android:foreground="?selectableItemBackground"
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_opacity_black_24dp"
android:tint="@color/icon_color" android:tint="@color/icon_color"
android:foreground="?selectableItemBackground"
app:layout_constraintStart_toEndOf="@id/image_draw_color"
app:layout_constraintEnd_toStartOf="@id/image_draw_undo" app:layout_constraintEnd_toStartOf="@id/image_draw_undo"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintStart_toEndOf="@id/image_draw_color"
app:layout_constraintTop_toTopOf="parent" />
<ImageView <ImageView
android:id="@+id/image_draw_undo" android:id="@+id/image_draw_undo"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"
android:src="@drawable/ic_undo_black_24dp" android:foreground="?selectableItemBackground"
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_undo_black_24dp"
android:tint="@color/icon_color" android:tint="@color/icon_color"
android:foreground="?selectableItemBackground"
app:layout_constraintStart_toEndOf="@id/image_draw_opacity"
app:layout_constraintEnd_toStartOf="@id/image_draw_redo" app:layout_constraintEnd_toStartOf="@id/image_draw_redo"
app:layout_constraintStart_toEndOf="@id/image_draw_opacity"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ImageView <ImageView
android:id="@+id/image_draw_redo" android:id="@+id/image_draw_redo"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"
android:src="@drawable/ic_redo_black_24dp" android:foreground="?selectableItemBackground"
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_redo_black_24dp"
android:tint="@color/icon_color" android:tint="@color/icon_color"
android:foreground="?selectableItemBackground"
app:layout_constraintStart_toEndOf="@id/image_draw_undo"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/image_draw_undo"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<SeekBar <SeekBar
android:id="@+id/seekBar_width" android:id="@+id/seekBar_width"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="56dp" android:layout_height="56dp"
app:layout_constraintStart_toStartOf="parent" android:paddingStart="16dp"
app:layout_constraintEnd_toEndOf="parent" android:paddingEnd="16dp"
app:layout_constraintTop_toBottomOf="@id/image_draw_eraser" android:progress="8"
android:progressTint="@color/colorAccent" android:progressTint="@color/colorAccent"
android:thumbTint="@color/colorAccent" android:thumbTint="@color/colorAccent"
android:progress="8" app:layout_constraintEnd_toEndOf="parent"
android:paddingStart="16dp" app:layout_constraintStart_toStartOf="parent"
android:paddingEnd="16dp" /> app:layout_constraintTop_toBottomOf="@id/image_draw_eraser" />
<SeekBar <SeekBar
android:id="@+id/seekBar_opacity" android:id="@+id/seekBar_opacity"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="56dp" android:layout_height="56dp"
app:layout_constraintStart_toStartOf="parent" android:paddingStart="16dp"
app:layout_constraintEnd_toEndOf="parent" android:paddingEnd="16dp"
app:layout_constraintTop_toBottomOf="@id/image_draw_eraser"
android:progress="100" android:progress="100"
android:progressTint="@color/colorAccent" android:progressTint="@color/colorAccent"
android:thumbTint="@color/colorAccent" android:thumbTint="@color/colorAccent"
android:paddingStart="16dp" app:layout_constraintEnd_toEndOf="parent"
android:paddingEnd="16dp" /> app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/image_draw_eraser" />
<include <include
android:id="@+id/draw_color_palette" android:id="@+id/draw_color_palette"
layout="@layout/color_palette_view" layout="@layout/color_palette_view"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="56dp" android:layout_height="56dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/image_draw_eraser" /> app:layout_constraintTop_toBottomOf="@id/image_draw_eraser" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
......
<resources> <resources>
<string name="app_name">Draw</string> <string name="app_name">Draw</string>
// TODO: Add other translations like we did for the app module.
<string name="msg_wrong_processing_draw_image">Something was wrong while processing the draw image. Please, try again later.</string>
</resources> </resources>
include ':app', ':player', ':emoji', ':draw' //, ':wear' include ':app', ':player', ':emoji', ':draw', ':util', ':core' //, ':wear'
\ No newline at end of file \ No newline at end of file
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion versions.compileSdk
defaultConfig {
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
versionCode 1
versionName "1.0.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':core')
implementation libraries.kotlin
implementation libraries.coroutines
implementation libraries.coroutinesAndroid
// TODO This is a dependency from the core module, but the util module are unable to get that dependencies. Check why it is occurring since transitive is enable by default
implementation libraries.lifecycleExtensions
kapt libraries.lifecycleCompiler
}
\ No newline at end of file
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.rocket.android.util" />
package chat.rocket.android.util.extensions package chat.rocket.android.util.extension
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import kotlinx.coroutines.experimental.CoroutineScope import kotlinx.coroutines.experimental.CoroutineScope
......
package chat.rocket.android.util.extension
import android.graphics.Bitmap
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.withContext
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
/**
* Compress a [Bitmap] image.
*
* @param mimeType The MimeType of what the compressed image should be.
* @return An [InputStream] of a compressed image, otherwise null if the compression couldn't be done.
*/
suspend fun Bitmap.compressImageAndGetInputStream(mimeType: String): InputStream? {
var inputStream: InputStream? = null
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 =
this.compress(mimeType.getCompressFormat(), 70, byteArrayOutputStream)
if (isCompressed) {
inputStream = ByteArrayInputStream(byteArrayOutputStream.toByteArray())
}
}
return inputStream
}
/**
* Compress a [Bitmap] image.
*
* @param mimeType The MimeType of what the compressed image should be.
* @return An [ByteArray] of a compressed image, otherwise null if the compression couldn't be done.
*/
suspend fun Bitmap.compressImageAndGetByteArray(mimeType: String): ByteArray? {
var byteArray: ByteArray? = null
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 =
this.compress(mimeType.getCompressFormat(), 70, byteArrayOutputStream)
if (isCompressed) {
byteArray = byteArrayOutputStream.toByteArray()
}
}
return byteArray
}
/**
* Gets the [Bitmap.CompressFormat] based on the image MimeType.
* Note: Supported formats are: PNG, JPEG and WEBP.
*/
fun String.getCompressFormat(): Bitmap.CompressFormat {
return when {
this.contains("jpeg") -> Bitmap.CompressFormat.JPEG
this.contains("webp") -> Bitmap.CompressFormat.WEBP
else -> Bitmap.CompressFormat.PNG
}
}
\ No newline at end of file
<resources>
<string name="app_name">Util</string>
</resources>
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