Commit c63e1da7 authored by Lucio Maciel's avatar Lucio Maciel

Initial message db models and DAO, save messages to database

parent cb57e76e
...@@ -5,9 +5,14 @@ import chat.rocket.android.chatroom.presentation.ChatRoomView ...@@ -5,9 +5,14 @@ import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.ui.ChatRoomFragment import chat.rocket.android.chatroom.ui.ChatRoomFragment
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.Job
import javax.inject.Named
@Module @Module
class ChatRoomFragmentModule { class ChatRoomFragmentModule {
...@@ -33,4 +38,22 @@ class ChatRoomFragmentModule { ...@@ -33,4 +38,22 @@ class ChatRoomFragmentModule {
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy { fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs) return CancelStrategy(owner, jobs)
} }
@Provides
@PerFragment
@Named("currentServer")
fun provideCurrentServer(currentServerInteractor: GetCurrentServerInteractor): String {
return currentServerInteractor.get()!!
}
@Provides
@PerFragment
fun provideDatabaseManager(factory: DatabaseManagerFactory,
@Named("currentServer") currentServer: String): DatabaseManager {
return factory.create(currentServer)
}
@Provides
@PerFragment
fun provideChatRoomDao(manager: DatabaseManager): ChatRoomDao = manager.chatRoomDao()
} }
...@@ -227,6 +227,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -227,6 +227,7 @@ class ChatRoomPresenter @Inject constructor(
retryIO("loadAndShowMessages($chatRoomId, $chatRoomType, $offset") { retryIO("loadAndShowMessages($chatRoomId, $chatRoomType, $offset") {
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
} }
dbManager.processMessagesBatch(messages)
messagesRepository.saveAll(messages) messagesRepository.saveAll(messages)
//we are saving last sync date of latest synced chat room message //we are saving last sync date of latest synced chat room message
...@@ -510,6 +511,8 @@ class ChatRoomPresenter @Inject constructor( ...@@ -510,6 +511,8 @@ class ChatRoomPresenter @Inject constructor(
} }
Timber.d("History: $messages") Timber.d("History: $messages")
dbManager.processMessagesBatch(messages.result)
if (messages.result.isNotEmpty()) { if (messages.result.isNotEmpty()) {
val models = mapper.map(messages.result, RoomUiModel( val models = mapper.map(messages.result, RoomUiModel(
roles = chatRoles, roles = chatRoles,
......
...@@ -30,7 +30,6 @@ import chat.rocket.android.chatrooms.viewmodel.ChatRoomsViewModel ...@@ -30,7 +30,6 @@ 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.LoadingState
import chat.rocket.android.chatrooms.viewmodel.Query import chat.rocket.android.chatrooms.viewmodel.Query
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
...@@ -57,10 +56,10 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -57,10 +56,10 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject @Inject
lateinit var factory: ChatRoomsViewModelFactory lateinit var factory: ChatRoomsViewModelFactory
@Inject @Inject
lateinit var dbManager: DatabaseManager // TODO - remove when moving ChatRoom screen to DB
@Inject
lateinit var analyticsManager: AnalyticsManager lateinit var analyticsManager: AnalyticsManager
lateinit var viewModel: ChatRoomsViewModel
private lateinit var viewModel: ChatRoomsViewModel
private var searchView: SearchView? = null private var searchView: SearchView? = null
private var sortView: MenuItem? = null private var sortView: MenuItem? = null
private val handler = Handler() private val handler = Handler()
......
...@@ -99,7 +99,7 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> { ...@@ -99,7 +99,7 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> {
abstract fun update(list: List<ChatRoomEntity>) abstract fun update(list: List<ChatRoomEntity>)
@Transaction @Transaction
open fun update(toRemove: List<String>, toInsert: List<ChatRoomEntity>, toUpdate: List<ChatRoomEntity>) { open fun update(toInsert: List<ChatRoomEntity>, toUpdate: List<ChatRoomEntity>, toRemove: List<String>) {
insertOrReplace(toInsert) insertOrReplace(toInsert)
update(toUpdate) update(toUpdate)
toRemove.forEach { id -> toRemove.forEach { id ->
......
package chat.rocket.android.db
import chat.rocket.android.infrastructure.MessagesRepository
import chat.rocket.core.model.Message
class DatabaseMessagesRepository(val dbManager: DatabaseManager) : MessagesRepository {
override fun saveMessage(message: Message) {
saveMessages(listOf(message))
}
override fun saveMessages(messages: List<Message>) {
messages.forEach { message ->
}
}
}
\ No newline at end of file
package chat.rocket.android.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import chat.rocket.android.db.model.AttachmentEntity
import chat.rocket.android.db.model.AttachmentFieldEntity
import chat.rocket.android.db.model.BaseMessageEntity
import chat.rocket.android.db.model.MessageChannelsRelation
import chat.rocket.android.db.model.MessageEntity
import chat.rocket.android.db.model.MessageFavoritesRelation
import chat.rocket.android.db.model.MessageMentionsRelation
import chat.rocket.android.db.model.ReactionEntity
import chat.rocket.android.db.model.ReactionMessageRelation
import chat.rocket.android.db.model.UrlEntity
import timber.log.Timber
@Dao
abstract class MessageDao {
@Insert
abstract fun insert(message: MessageEntity)
@Insert
abstract fun insert(relation: MessageFavoritesRelation)
@Insert
abstract fun insert(relation: MessageMentionsRelation)
@Insert
abstract fun insert(relation: MessageChannelsRelation)
@Insert
abstract fun insert(attachment: AttachmentEntity)
@Insert
abstract fun insert(field: AttachmentFieldEntity)
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(reaction: ReactionEntity)
@Insert
abstract fun insert(relation: ReactionMessageRelation)
@Insert
abstract fun insert(url: UrlEntity)
@Query("DELETE FROM messages WHERE id = :id")
abstract fun delete(id: String)
@Transaction
open fun insert(message: MessageEntity, entities: List<BaseMessageEntity>) {
insertInternal(message, entities)
}
private fun insertInternal(message: MessageEntity, entities: List<BaseMessageEntity>) {
Timber.d("Inserting message: ${message.id}, entities: ${entities.size}")
delete(message.id)
insert(message)
entities.forEach { entity ->
insert(entity)
}
}
private fun insert(entity: BaseMessageEntity) {
when(entity) {
is MessageEntity -> insert(entity)
is MessageFavoritesRelation -> insert(entity)
is MessageMentionsRelation -> insert(entity)
is MessageChannelsRelation -> insert(entity)
is AttachmentEntity -> insert(entity)
is AttachmentFieldEntity -> insert(entity)
is ReactionEntity -> insert(entity)
is ReactionMessageRelation -> insert(entity)
is UrlEntity -> insert(entity)
}
}
@Transaction
open fun insert(list: List<Pair<MessageEntity, List<BaseMessageEntity>>>) {
list.forEach { (message, entities) ->
insertInternal(message, entities)
}
}
}
\ No newline at end of file
...@@ -2,29 +2,31 @@ package chat.rocket.android.db ...@@ -2,29 +2,31 @@ package chat.rocket.android.db
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.migration.Migration import chat.rocket.android.db.model.AttachmentEntity
import androidx.sqlite.db.SupportSQLiteDatabase import chat.rocket.android.db.model.AttachmentFieldEntity
import chat.rocket.android.db.model.ChatRoomEntity import chat.rocket.android.db.model.ChatRoomEntity
import chat.rocket.android.db.model.MessageChannelsRelation
import chat.rocket.android.db.model.MessageEntity
import chat.rocket.android.db.model.MessageFavoritesRelation
import chat.rocket.android.db.model.MessageMentionsRelation
import chat.rocket.android.db.model.ReactionEntity
import chat.rocket.android.db.model.ReactionMessageRelation
import chat.rocket.android.db.model.UrlEntity
import chat.rocket.android.db.model.UserEntity import chat.rocket.android.db.model.UserEntity
@Database( @Database(
entities = [UserEntity::class, ChatRoomEntity::class], entities = [
version = 5, UserEntity::class, ChatRoomEntity::class, MessageEntity::class,
MessageFavoritesRelation::class, MessageMentionsRelation::class,
MessageChannelsRelation::class, AttachmentEntity::class,
AttachmentFieldEntity::class, UrlEntity::class, ReactionEntity::class,
ReactionMessageRelation::class
],
version = 6,
exportSchema = true exportSchema = true
) )
abstract class RCDatabase : RoomDatabase() { abstract class RCDatabase : RoomDatabase() {
abstract fun userDao(): UserDao abstract fun userDao(): UserDao
abstract fun chatRoomDao(): ChatRoomDao abstract fun chatRoomDao(): ChatRoomDao
abstract fun messageDao(): MessageDao
companion object {
@JvmField
val MIGRATION_4_5 = Migration4to5()
}
}
class Migration4to5 : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE INDEX `index_chatrooms_lastMessageUserId` ON `chatrooms` (`lastMessageUserId`)")
}
} }
\ No newline at end of file
package chat.rocket.android.db.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(tableName = "attachments",
foreignKeys = [
ForeignKey(entity = MessageEntity::class, parentColumns = ["id"],
childColumns = ["message_id"], onDelete = ForeignKey.CASCADE)
])
data class AttachmentEntity(
@PrimaryKey
val id: String,
@ColumnInfo(name = "message_id")
val messageId: String,
val title: String?,
val type: String?,
val description: String?,
val text: String?,
@ColumnInfo(name = "author_name")
val authorName: String?,
@ColumnInfo(name = "author_icon")
val authorIcon: String?,
@ColumnInfo(name = "author_link")
val authorLink: String?,
@ColumnInfo(name = "thumb_url")
val thumbUrl: String?,
val color: String?,
@ColumnInfo(name = "title_link")
val titleLink: String?,
@ColumnInfo(name = "title_link_download")
val titleLinkDownload: String?,
@ColumnInfo(name = "image_url")
val imageUrl: String?,
@ColumnInfo(name = "image_type")
val imageType: String?,
@ColumnInfo(name = "image_size")
val imageSize: String?,
@ColumnInfo(name = "video_url")
val videoUrl: String?,
@ColumnInfo(name = "video_type")
val videoType: String?,
@ColumnInfo(name = "video_size")
val videoSize: String?,
@ColumnInfo(name = "audio_url")
val audioUrl: String?,
@ColumnInfo(name = "audio_type")
val audioType: String?,
@ColumnInfo(name = "audio_size")
val audioSize: String?,
@ColumnInfo(name = "message_link")
val messageLink: String?,
val timestamp: Long?
) : BaseMessageEntity
@Entity(tableName = "attachment_fields",
foreignKeys = [
ForeignKey(entity = AttachmentEntity::class, parentColumns = ["id"],
childColumns = ["attachmentId"], onDelete = ForeignKey.CASCADE)
])
data class AttachmentFieldEntity(
val attachmentId: String,
val title: String,
val value: String
) : BaseMessageEntity {
@PrimaryKey(autoGenerate = true)
var id: Long? = null
}
\ No newline at end of file
package chat.rocket.android.db.model
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
interface BaseMessageEntity
@Entity(tableName = "messages",
foreignKeys = [
ForeignKey(entity = UserEntity::class, parentColumns = ["id"], childColumns = ["senderId"]),
ForeignKey(entity = UserEntity::class, parentColumns = ["id"], childColumns = ["editedBy"])
])
data class MessageEntity(
@PrimaryKey val id: String,
val roomId: String,
val message: String,
val timestamp: Long,
val senderId: String?,
val updatedAt: Long?,
val editedAt: Long?,
val editedBy: String?,
val senderAlias: String?,
val avatar: String?,
val type: String?,
val groupable: Boolean = false,
val parseUrls: Boolean = false,
val pinned: Boolean = false,
val role: String?
) : BaseMessageEntity
@Entity(tableName = "message_favorites",
primaryKeys = ["messageId", "userId"],
foreignKeys = [
ForeignKey(entity = MessageEntity::class, parentColumns = ["id"],
childColumns = ["messageId"], onDelete = ForeignKey.CASCADE),
ForeignKey(entity = UserEntity::class, parentColumns = ["id"], childColumns = ["userId"])
])
data class MessageFavoritesRelation(
val messageId: String,
val userId: String
) : BaseMessageEntity
@Entity(tableName = "message_mentions",
primaryKeys = ["messageId", "userId"],
foreignKeys = [
ForeignKey(entity = MessageEntity::class, parentColumns = ["id"],
childColumns = ["messageId"], onDelete = ForeignKey.CASCADE),
ForeignKey(entity = UserEntity::class, parentColumns = ["id"], childColumns = ["userId"])
])
data class MessageMentionsRelation(
val messageId: String,
val userId: String
) : BaseMessageEntity
@Entity(tableName = "message_channels",
primaryKeys = ["messageId", "roomId"],
foreignKeys = [
ForeignKey(entity = MessageEntity::class, parentColumns = ["id"],
childColumns = ["messageId"], onDelete = ForeignKey.CASCADE)
]
)
data class MessageChannelsRelation(
val messageId: String,
val roomId: String,
val roomName: String?
) : BaseMessageEntity
package chat.rocket.android.db.model
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "reactions")
data class ReactionEntity(
@PrimaryKey val reaction: String
) : BaseMessageEntity
@Entity(tableName = "reactions_message_relations",
foreignKeys = [
ForeignKey(entity = ReactionEntity::class, parentColumns = ["reaction"],
childColumns = ["reactionId"]),
ForeignKey(entity = MessageEntity::class, parentColumns = ["id"],
childColumns = ["messageId"], onDelete = ForeignKey.CASCADE)
],
indices = [
Index(value = ["messageId"])
]
)
data class ReactionMessageRelation(
val reactionId: String,
val messageId: String,
val count: Int
) : BaseMessageEntity {
@PrimaryKey(autoGenerate = true)
var id: Long? = null
}
\ No newline at end of file
package chat.rocket.android.db.model
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "urls",
foreignKeys = [
ForeignKey(entity = MessageEntity::class, parentColumns = ["id"],
childColumns = ["messageId"], onDelete = ForeignKey.CASCADE)
],
indices = [
Index(value = ["messageId"])
])
data class UrlEntity(
val messageId: String,
val url: String,
val hostname: String?,
val title: String?,
val description: String?,
val imageUrl: String?
) : BaseMessageEntity {
@PrimaryKey(autoGenerate = true)
var id: Long? = null
}
\ No newline at end of file
package chat.rocket.android.infrastructure
import chat.rocket.core.model.Message
interface MessagesRepository {
fun saveMessage(message: Message)
fun saveMessages(messages: List<Message>)
}
\ No newline at end of file
...@@ -32,7 +32,6 @@ import kotlinx.coroutines.experimental.selects.select ...@@ -32,7 +32,6 @@ import kotlinx.coroutines.experimental.selects.select
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import kotlin.coroutines.experimental.CoroutineContext import kotlin.coroutines.experimental.CoroutineContext
import kotlin.math.absoluteValue
class ConnectionManager( class ConnectionManager(
internal val client: RocketChatClient, internal val client: RocketChatClient,
...@@ -55,6 +54,7 @@ class ConnectionManager( ...@@ -55,6 +54,7 @@ class ConnectionManager(
private val activeUsersContext = newSingleThreadContext("activeUsersContext") private val activeUsersContext = newSingleThreadContext("activeUsersContext")
private val roomsContext = newSingleThreadContext("roomsContext") private val roomsContext = newSingleThreadContext("roomsContext")
private val messagesContext = newSingleThreadContext("messagesContext")
fun connect() { fun connect() {
if (connectJob?.isActive == true && (state !is State.Disconnected)) { if (connectJob?.isActive == true && (state !is State.Disconnected)) {
...@@ -125,7 +125,13 @@ class ConnectionManager( ...@@ -125,7 +125,13 @@ class ConnectionManager(
val roomsActor = createBatchActor<StreamMessage<BaseRoom>>(roomsContext, parent = connectJob, val roomsActor = createBatchActor<StreamMessage<BaseRoom>>(roomsContext, parent = connectJob,
maxSize = 10) { batch -> maxSize = 10) { batch ->
Timber.d("processing Stream batch: ${batch.size} - $batch") Timber.d("processing Stream batch: ${batch.size} - $batch")
dbManager.processStreamBatch(batch) dbManager.processChatRoomsBatch(batch)
}
val messagesActor = createBatchActor<Message>(messagesContext, parent = connectJob,
maxSize = 100, maxTime = 300) { messages ->
Timber.d("Processing Messages batch: ${messages.size}")
dbManager.processMessagesBatch(messages)
} }
// stream-notify-user - ${userId}/rooms-changed // stream-notify-user - ${userId}/rooms-changed
...@@ -148,6 +154,7 @@ class ConnectionManager( ...@@ -148,6 +154,7 @@ class ConnectionManager(
launch(parent = connectJob) { launch(parent = connectJob) {
for (message in client.messagesChannel) { for (message in client.messagesChannel) {
Timber.d("Received new Message for room ${message.roomId}") Timber.d("Received new Message for room ${message.roomId}")
messagesActor.send(message)
val channel = roomMessagesChannels[message.roomId] val channel = roomMessagesChannels[message.roomId]
channel?.send(message) channel?.send(message)
} }
...@@ -164,12 +171,9 @@ class ConnectionManager( ...@@ -164,12 +171,9 @@ class ConnectionManager(
} }
} }
var totalUsers = 0
// activeUsers // activeUsers
launch(parent = connectJob) { launch(parent = connectJob) {
for (user in client.activeUsersChannel) { for (user in client.activeUsersChannel) {
totalUsers++
//Timber.d("Got activeUsers: $totalUsers")
userActor.send(user) userActor.send(user)
} }
} }
......
package chat.rocket.android.util.extensions package chat.rocket.android.util.extensions
import chat.rocket.android.db.model.MessageEntity
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.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.registerPushToken import chat.rocket.core.internal.rest.registerPushToken
import chat.rocket.core.model.Message
import chat.rocket.core.model.asString
import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext import kotlinx.coroutines.experimental.withContext
...@@ -27,3 +30,23 @@ suspend fun RocketChatClientFactory.registerPushToken( ...@@ -27,3 +30,23 @@ suspend fun RocketChatClientFactory.registerPushToken(
} }
} }
} }
fun Message.toEntity(): MessageEntity {
return MessageEntity(
id = id,
roomId = roomId,
message = message,
timestamp = timestamp,
senderId = sender?.id,
updatedAt = updatedAt,
editedAt = editedAt,
editedBy = editedBy?.id,
senderAlias = senderAlias,
avatar = avatar,
type = type.asString(),
groupable = groupable,
parseUrls = parseUrls,
pinned = pinned,
role = role
)
}
\ No newline at end of file
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