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
import chat.rocket.android.chatroom.ui.ChatRoomFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
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.Provides
import kotlinx.coroutines.experimental.Job
import javax.inject.Named
@Module
class ChatRoomFragmentModule {
......@@ -33,4 +38,22 @@ class ChatRoomFragmentModule {
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
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(
retryIO("loadAndShowMessages($chatRoomId, $chatRoomType, $offset") {
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
}
dbManager.processMessagesBatch(messages)
messagesRepository.saveAll(messages)
//we are saving last sync date of latest synced chat room message
......@@ -510,6 +511,8 @@ class ChatRoomPresenter @Inject constructor(
}
Timber.d("History: $messages")
dbManager.processMessagesBatch(messages.result)
if (messages.result.isNotEmpty()) {
val models = mapper.map(messages.result, RoomUiModel(
roles = chatRoles,
......
......@@ -30,7 +30,6 @@ import chat.rocket.android.chatrooms.viewmodel.ChatRoomsViewModel
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.helper.ChatRoomsSortOrder
import chat.rocket.android.helper.Constants
import chat.rocket.android.helper.SharedPreferenceHelper
......@@ -57,10 +56,10 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject
lateinit var factory: ChatRoomsViewModelFactory
@Inject
lateinit var dbManager: DatabaseManager // TODO - remove when moving ChatRoom screen to DB
@Inject
lateinit var analyticsManager: AnalyticsManager
lateinit var viewModel: ChatRoomsViewModel
private lateinit var viewModel: ChatRoomsViewModel
private var searchView: SearchView? = null
private var sortView: MenuItem? = null
private val handler = Handler()
......
......@@ -99,7 +99,7 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> {
abstract fun update(list: List<ChatRoomEntity>)
@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)
update(toUpdate)
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
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import chat.rocket.android.db.model.AttachmentEntity
import chat.rocket.android.db.model.AttachmentFieldEntity
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
@Database(
entities = [UserEntity::class, ChatRoomEntity::class],
version = 5,
entities = [
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
)
abstract class RCDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun chatRoomDao(): ChatRoomDao
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`)")
}
abstract fun messageDao(): MessageDao
}
\ 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
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.coroutines.experimental.CoroutineContext
import kotlin.math.absoluteValue
class ConnectionManager(
internal val client: RocketChatClient,
......@@ -55,6 +54,7 @@ class ConnectionManager(
private val activeUsersContext = newSingleThreadContext("activeUsersContext")
private val roomsContext = newSingleThreadContext("roomsContext")
private val messagesContext = newSingleThreadContext("messagesContext")
fun connect() {
if (connectJob?.isActive == true && (state !is State.Disconnected)) {
......@@ -125,7 +125,13 @@ class ConnectionManager(
val roomsActor = createBatchActor<StreamMessage<BaseRoom>>(roomsContext, parent = connectJob,
maxSize = 10) { 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
......@@ -148,6 +154,7 @@ class ConnectionManager(
launch(parent = connectJob) {
for (message in client.messagesChannel) {
Timber.d("Received new Message for room ${message.roomId}")
messagesActor.send(message)
val channel = roomMessagesChannels[message.roomId]
channel?.send(message)
}
......@@ -164,12 +171,9 @@ class ConnectionManager(
}
}
var totalUsers = 0
// activeUsers
launch(parent = connectJob) {
for (user in client.activeUsersChannel) {
totalUsers++
//Timber.d("Got activeUsers: $totalUsers")
userActor.send(user)
}
}
......
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.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO
import chat.rocket.core.RocketChatClient
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.launch
import kotlinx.coroutines.experimental.withContext
......@@ -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