Commit c319c1a9 authored by Lucio Maciel's avatar Lucio Maciel

Spotlight, loading and empty indicators, fixes on rooms synchronization

and other fixes
parent a8daa6c8
...@@ -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 {
......
{
"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
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,9 +15,9 @@ class ChatRoomsRepository @Inject constructor(val dao: ChatRoomDao){ ...@@ -15,9 +15,9 @@ class ChatRoomsRepository @Inject constructor(val dao: ChatRoomDao){
} }
} }
fun getChatRooms(query: String): List<ChatRoom> { fun search(query: String) = dao.searchSync(query)
TODO()
} fun count() = dao.count()
enum class Order { enum class Order {
ACTIVITY, 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
...@@ -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
......
...@@ -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,21 +5,30 @@ import androidx.lifecycle.MutableLiveData ...@@ -5,21 +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.db.model.ChatRoom
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.filter
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,
...@@ -27,38 +36,65 @@ class ChatRoomsViewModel( ...@@ -27,38 +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>()
private val query: MutableLiveData<String> = MutableLiveData() 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
}
}
} }
} }
fun spotlight(): LiveData<List<ChatRoom>> { private suspend fun spotlight(query: String): SpotlightResult? {
return query.filter { !it.isNullOrEmpty() }.distinct().nonNull().transform { return try {
repository.getChatRooms(it!!) 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
...@@ -67,11 +103,69 @@ class ChatRoomsViewModel( ...@@ -67,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 setOrdering(order: ChatRoomsRepository.Order) { fun setQuery(query: Query) {
ordering.value = order 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 Query.isGrouped(): Boolean {
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")
} }
} }
...@@ -24,6 +24,13 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> { ...@@ -24,6 +24,13 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> {
@Query("$BASE_QUERY") @Query("$BASE_QUERY")
abstract fun getAllSync(): List<ChatRoom> 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
...@@ -66,6 +73,15 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> { ...@@ -66,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(
......
...@@ -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.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
...@@ -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
...@@ -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
......
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.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
<?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
...@@ -281,4 +281,5 @@ ...@@ -281,4 +281,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>
...@@ -282,4 +282,5 @@ ...@@ -282,4 +282,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>
...@@ -266,4 +266,5 @@ ...@@ -266,4 +266,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
...@@ -261,4 +261,5 @@ ...@@ -261,4 +261,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>
...@@ -258,4 +258,5 @@ ...@@ -258,4 +258,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>
...@@ -119,6 +119,7 @@ ...@@ -119,6 +119,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>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">Private</string> <string name="msg_private_channel">Private</string>
......
...@@ -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',
......
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