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

Merge pull request #1669 from RocketChat/feature/db-save-messages

[FEATURE] Initial message db support
parents 6d35b48e e31ae695
......@@ -152,6 +152,8 @@ dependencies {
implementation libraries.livedataKtx
implementation 'com.google.code.findbugs:jsr305:3.0.2'
// Proprietary libraries
playImplementation libraries.fcm
playImplementation libraries.firebaseAnalytics
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
673693445664
673693445664
......@@ -326,7 +326,7 @@ class LoginPresenter @Inject constructor(
getCustomOauthScope(serviceMap) ?: "openid"
)
}
wordpressOauthUrl?.let {
wordpressOauthUrl.let {
view.setupWordpressButtonListener(it, state)
view.enableLoginByWordpress()
totalSocialAccountsEnabled++
......
......@@ -51,7 +51,7 @@ class ActionsListAdapter(actions: List<Action>, var actionAttachmentOnClickListe
action_button.isVisible = true
action_image_button.isVisible = false
this.action_button.setText(action.text)
this.action_button.text = action.text
}
}
}
......
......@@ -190,7 +190,7 @@ class ChatRoomAdapter(
notifyDataSetChanged()
}
fun updateItem(message: BaseUiModel<*>) {
fun updateItem(message: BaseUiModel<*>): Boolean {
val index = dataSet.indexOfLast { it.messageId == message.messageId }
val indexOfNext = dataSet.indexOfFirst { it.messageId == message.messageId }
Timber.d("index: $index")
......@@ -209,7 +209,9 @@ class ChatRoomAdapter(
dataSet.removeAt(indexOfNext)
notifyItemRemoved(indexOfNext)
}
return true
}
return false
}
fun removeItem(messageId: String) {
......
package chat.rocket.android.chatroom.adapter
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.ColorAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
......@@ -15,8 +18,7 @@ class ColorAttachmentViewHolder(itemView: View,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ColorAttachmentUiModel>(itemView, listener, reactionListener) {
val drawable: Drawable? = ContextCompat.getDrawable(itemView.context,
R.drawable.quote_vertical_gray_bar)
val drawable: Drawable = ColorDrawable(ContextCompat.getColor(itemView.context, R.color.quoteBar))
init {
with(itemView) {
......@@ -27,10 +29,20 @@ class ColorAttachmentViewHolder(itemView: View,
override fun bindViews(data: ColorAttachmentUiModel) {
with(itemView) {
drawable?.let {
quote_bar.background = drawable.mutate().apply { setTint(data.color) }
attachment_text.text = data.text
}
quote_bar.setColorFilter(data.color)
if (data.text.isNotEmpty()) {
attachment_text.isVisible = true
attachment_text.text = data.text
} else {
attachment_text.isVisible = false
}
if (data.fields.isNullOrEmpty()) {
text_fields.isVisible = false
} else {
text_fields.isVisible = true
text_fields.text = data.fields
}
}
}
......
......@@ -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,8 @@ class ChatRoomFragmentModule {
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
@Provides
@PerFragment
fun provideChatRoomDao(manager: DatabaseManager): ChatRoomDao = manager.chatRoomDao()
}
......@@ -28,7 +28,6 @@ import chat.rocket.android.server.domain.JobSchedulerInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.RoomRepository
import chat.rocket.android.server.domain.UsersRepository
import chat.rocket.android.server.domain.uploadMaxFileSize
import chat.rocket.android.server.domain.uploadMimeTypeFilter
......@@ -92,7 +91,6 @@ class ChatRoomPresenter @Inject constructor(
private val uriInteractor: UriInteractor,
private val messagesRepository: MessagesRepository,
private val usersRepository: UsersRepository,
private val roomsRepository: RoomRepository,
private val localRepository: LocalRepository,
private val analyticsManager: AnalyticsManager,
private val userHelper: UserHelper,
......@@ -303,7 +301,7 @@ class ChatRoomPresenter @Inject constructor(
type = null,
updatedAt = null,
urls = null,
isTemporary = true,
synced = false,
unread = true
)
try {
......@@ -315,6 +313,7 @@ class ChatRoomPresenter @Inject constructor(
), false
)
client.sendMessage(id, chatRoomId, text)
messagesRepository.save(newMessage.copy(synced = true))
logMessageSent()
} catch (ex: Exception) {
// Ok, not very beautiful, but the backend sends us a not valid response
......@@ -504,7 +503,7 @@ class ChatRoomPresenter @Inject constructor(
val messages =
retryIO(description = "history($chatRoomId, $roomType, $instant)") {
client.history(
chatRoomId!!, roomType, count = 50,
chatRoomId, roomType, count = 50,
oldest = instant
)
}
......@@ -713,6 +712,7 @@ class ChatRoomPresenter @Inject constructor(
client.getMembers(chatRoomId, roomTypeOf(chatRoomType), offset, 50).result
}.take(50) // Get only 50, the backend is returning 7k+ users
usersRepository.saveAll(members)
dbManager.processUsersBatch(members)
val self = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
// Take at most the 100 most recent messages distinguished by user. Can return less.
val recentMessages = messagesRepository.getRecentMessages(chatRoomId, 100)
......@@ -785,9 +785,6 @@ class ChatRoomPresenter @Inject constructor(
}.filterNot { filterSelfOut && self != null && self == it.text })
}
ROOMS -> {
if (rooms.isNotEmpty()) {
roomsRepository.saveAll(rooms)
}
view.populateRoomSuggestions(rooms.map {
val fullName = it.fullName ?: ""
val name = it.name ?: ""
......@@ -1125,11 +1122,11 @@ class ChatRoomPresenter @Inject constructor(
val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id }
if (index > -1) {
Timber.d("Updating message at $index")
messagesRepository.save(streamedMessage)
//messagesRepository.save(streamedMessage)
view.dispatchUpdateMessage(index, viewModelStreamedMessage)
} else {
Timber.d("Adding new message")
messagesRepository.save(streamedMessage)
//messagesRepository.save(streamedMessage)
view.showNewMessage(viewModelStreamedMessage, true)
}
}
......
......@@ -2,9 +2,11 @@ package chat.rocket.android.chatroom.service
import android.app.job.JobParameters
import android.app.job.JobService
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.DatabaseMessageMapper
import chat.rocket.android.server.infraestructure.DatabaseMessagesRepository
import chat.rocket.core.internal.rest.sendMessage
import chat.rocket.core.model.Message
import dagger.android.AndroidInjection
......@@ -17,9 +19,9 @@ class MessageService : JobService() {
@Inject
lateinit var factory: ConnectionManagerFactory
@Inject
lateinit var currentServerRepository: CurrentServerRepository
lateinit var dbFactory: DatabaseManagerFactory
@Inject
lateinit var messageRepository: MessagesRepository
lateinit var getAccountsInteractor: GetAccountsInteractor
override fun onCreate() {
super.onCreate()
......@@ -32,21 +34,21 @@ class MessageService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
launch(CommonPool) {
val currentServer = currentServerRepository.get()
if (currentServer != null) {
retrySendingMessages(params, currentServer)
jobFinished(params, false)
getAccountsInteractor.get().forEach { account ->
retrySendingMessages(params, account.serverUrl)
}
jobFinished(params, false)
}
return true
}
private suspend fun retrySendingMessages(params: JobParameters?, currentServer: String) {
private suspend fun retrySendingMessages(params: JobParameters?, serverUrl: String) {
val dbManager = dbFactory.create(serverUrl)
val messageRepository = DatabaseMessagesRepository(dbManager, DatabaseMessageMapper(dbManager))
val temporaryMessages = messageRepository.getAllUnsent()
.sortedWith(compareBy(Message::timestamp))
if (temporaryMessages.isNotEmpty()) {
val connectionManager = factory.create(currentServer)
val client = connectionManager.client
val client = factory.create(serverUrl).client
temporaryMessages.forEach { message ->
try {
client.sendMessage(
......@@ -57,7 +59,7 @@ class MessageService : JobService() {
attachments = message.attachments,
alias = message.senderAlias
)
messageRepository.save(message.copy(isTemporary = false))
messageRepository.save(message.copy(synced = true))
Timber.d("Sent scheduled message given by id: ${message.id}")
} catch (ex: Exception) {
Timber.e(ex)
......@@ -71,7 +73,7 @@ class MessageService : JobService() {
// some other error
if (ex.message?.contains("E11000", true) == true) {
// XXX: Temporary solution. We need proper error codes from the api.
messageRepository.save(message.copy(isTemporary = false))
messageRepository.save(message.copy(synced = false))
}
jobFinished(params, true)
}
......
......@@ -545,9 +545,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun dispatchUpdateMessage(index: Int, message: List<BaseUiModel<*>>) {
ui {
adapter.updateItem(message.last())
if (message.size > 1) {
adapter.prependData(listOf(message.first()))
if (adapter.updateItem(message.last())) {
if (message.size > 1) {
adapter.prependData(listOf(message.first()))
}
} else {
showNewMessage(message, true)
}
}
}
......
......@@ -9,6 +9,7 @@ data class ColorAttachmentUiModel(
val id: Long,
val color: Int,
val text: CharSequence,
val fields: CharSequence? = null,
override val message: Message,
override val rawData: ColorAttachment,
override val messageId: String,
......
......@@ -42,6 +42,7 @@ import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.attachment.AudioAttachment
import chat.rocket.core.model.attachment.AuthorAttachment
import chat.rocket.core.model.attachment.ColorAttachment
import chat.rocket.core.model.attachment.Field
import chat.rocket.core.model.attachment.FileAttachment
import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
......@@ -129,14 +130,14 @@ class UiModelMapper @Inject constructor(
withContext(CommonPool) {
val list = ArrayList<BaseUiModel<*>>()
message.urls?.forEach {
val url = mapUrl(message, it)
url?.let { list.add(url) }
message.urls?.forEach { url ->
mapUrl(message, url)?.let { list.add(it) }
}
message.attachments?.forEach {
val attachment = mapAttachment(message, it)
attachment?.let { list.add(attachment) }
message.attachments?.mapNotNull { attachment ->
mapAttachment(message, attachment)
}?.asReversed()?.let {
list.addAll(it)
}
mapMessage(message).let {
......@@ -175,7 +176,7 @@ class UiModelMapper @Inject constructor(
broadcast = broadcast ?: false,
alert = alert,
fullName = fullname,
name = name ?: "",
name = name,
favorite = favorite ?: false,
default = isDefault ?: false,
readonly = readonly,
......@@ -334,34 +335,39 @@ class UiModelMapper @Inject constructor(
val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp)
val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context)
val fieldsText = mapFields(fields)
ColorAttachmentUiModel(attachmentUrl = url, id = id, color = color.color,
text = text, message = message, rawData = attachment,
text = text, fields = fieldsText, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message), unread = message.unread,
showDayMarker = false, currentDayMarkerText = dayMarkerText)
}
}
private fun mapAuthorAttachment(message: Message, attachment: AuthorAttachment): AuthorAttachmentUiModel {
return with(attachment) {
val content = stripMessageQuotes(message)
val fieldsText = fields?.let {
buildSpannedString {
it.forEachIndexed { index, field ->
bold { append(field.title) }
append("\n")
if (field.value.isNotEmpty()) {
append(field.value)
}
private fun mapFields(fields: List<Field>?): CharSequence? {
return fields?.let {
buildSpannedString {
it.forEachIndexed { index, field ->
bold { append(field.title) }
append("\n")
if (field.value.isNotEmpty()) {
append(field.value)
}
if (index != it.size - 1) { // it is not the last one, append a new line
append("\n\n")
}
if (index != it.size - 1) { // it is not the last one, append a new line
append("\n\n")
}
}
}
}
}
private fun mapAuthorAttachment(message: Message, attachment: AuthorAttachment): AuthorAttachmentUiModel {
return with(attachment) {
val content = stripMessageQuotes(message)
val fieldsText = mapFields(fields)
val id = attachmentId(message, attachment)
val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp)
......@@ -477,7 +483,7 @@ class UiModelMapper @Inject constructor(
val time = getTime(message.timestamp)
val avatar = getUserAvatar(message)
val preview = mapMessagePreview(message)
val isTemp = message.isTemporary ?: false
val synced = message.synced
val unread = if (settings.messageReadReceiptEnabled()) {
message.unread ?: false
} else {
......@@ -492,7 +498,7 @@ class UiModelMapper @Inject constructor(
messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, currentDayMarkerText = dayMarkerText,
showDayMarker = false, reactions = getReactions(message), isFirstUnread = false,
preview = preview, isTemporary = isTemp, unread = unread)
preview = preview, isTemporary = !synced, unread = unread)
}
private fun mapMessagePreview(message: Message): Message {
......
......@@ -5,5 +5,4 @@ import chat.rocket.android.suggestions.model.SuggestionModel
class ChatRoomSuggestionUiModel(text: String,
val fullName: String,
val name: String,
searchList: List<String>) : SuggestionModel(text, searchList, false) {
}
\ No newline at end of file
searchList: List<String>) : SuggestionModel(text, searchList, false)
\ No newline at end of file
......@@ -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()
......
......@@ -9,7 +9,6 @@ import android.content.Context
import android.content.SharedPreferences
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.analytics.Analytics
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.AnswersAnalytics
import chat.rocket.android.analytics.GoogleAnalyticsForFirebase
......@@ -26,7 +25,6 @@ import chat.rocket.android.infrastructure.SharedPreferencesLocalRepository
import chat.rocket.android.push.GroupedPush
import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.ActiveUsersRepository
import chat.rocket.android.server.domain.AnalyticsTrackingInteractor
import chat.rocket.android.server.domain.AnalyticsTrackingRepository
import chat.rocket.android.server.domain.ChatRoomsRepository
......@@ -39,17 +37,15 @@ import chat.rocket.android.server.domain.JobSchedulerInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.PermissionsRepository
import chat.rocket.android.server.domain.RoomRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.UsersRepository
import chat.rocket.android.server.infraestructure.DatabaseMessageMapper
import chat.rocket.android.server.infraestructure.DatabaseMessagesRepository
import chat.rocket.android.server.infraestructure.JobSchedulerInteractorImpl
import chat.rocket.android.server.infraestructure.MemoryActiveUsersRepository
import chat.rocket.android.server.infraestructure.MemoryChatRoomsRepository
import chat.rocket.android.server.infraestructure.MemoryRoomRepository
import chat.rocket.android.server.infraestructure.MemoryUsersRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesAccountsRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesMessagesRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesPermissionsRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesSettingsRepository
import chat.rocket.android.server.infraestructure.SharedPrefsAnalyticsTrackingRepository
......@@ -201,24 +197,12 @@ class AppModule {
return SharedPreferencesPermissionsRepository(localRepository, moshi)
}
@Provides
@Singleton
fun provideRoomRepository(): RoomRepository {
return MemoryRoomRepository()
}
@Provides
@Singleton
fun provideChatRoomRepository(): ChatRoomsRepository {
return MemoryChatRoomsRepository()
}
@Provides
@Singleton
fun provideActiveUsersRepository(): ActiveUsersRepository {
return MemoryActiveUsersRepository()
}
@Provides
@Singleton
fun provideMoshi(
......@@ -254,13 +238,8 @@ class AppModule {
}
@Provides
@Singleton
fun provideMessageRepository(
@ForMessages preferences: SharedPreferences,
moshi: Moshi,
currentServerInteractor: GetCurrentServerInteractor
): MessagesRepository {
return SharedPreferencesMessagesRepository(preferences, moshi, currentServerInteractor)
fun provideMessageRepository(databaseManager: DatabaseManager): MessagesRepository {
return DatabaseMessagesRepository(databaseManager, DatabaseMessageMapper(databaseManager))
}
@Provides
......
......@@ -4,7 +4,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
interface BaseDao<T> {
@Insert
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg obj: T)
@Insert(onConflict = OnConflictStrategy.REPLACE)
......
......@@ -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 ->
......
This diff is collapsed.
......@@ -2,29 +2,32 @@ 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.AttachmentActionEntity
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.MessageChannels
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.MessagesSync
import chat.rocket.android.db.model.ReactionEntity
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,
MessageChannels::class, AttachmentEntity::class,
AttachmentFieldEntity::class, AttachmentActionEntity::class, UrlEntity::class,
ReactionEntity::class, MessagesSync::class
],
version = 9,
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
This diff is collapsed.
This diff is collapsed.
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",
foreignKeys = [
ForeignKey(entity = MessageEntity::class, parentColumns = ["id"],
childColumns = ["messageId"], onDelete = ForeignKey.CASCADE)
],
indices = [
Index(value = ["messageId"])
]
)
data class ReactionEntity(
@PrimaryKey val reaction: String,
val messageId: String,
val count: Int,
val usernames: String
) : BaseMessageEntity
\ No newline at end of file
This diff is collapsed.
package chat.rocket.android.server.domain
import chat.rocket.common.model.User
interface ActiveUsersRepository {
fun save(url: String, activeUsers: List<User>)
fun get(url: String): List<User>
}
\ No newline at end of file
This diff is collapsed.
package chat.rocket.android.util.extension
fun Boolean?.orFalse(): Boolean = this ?: false
\ 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