diff --git a/app/build.gradle b/app/build.gradle index 503b3eb4b..d486f35dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -158,7 +158,7 @@ ext { koin_version = "2.1.4" lifecycle_version = '2.2.0' coil_version = "0.9.5" - room_version = "2.2.4" + room_version = "2.2.5" } configurations.all { diff --git a/app/schemas/com.nextcloud.talk.newarch.local.db.TalkDatabase/1.json b/app/schemas/com.nextcloud.talk.newarch.local.db.TalkDatabase/1.json index bb2cd2235..3f745d5a6 100644 --- a/app/schemas/com.nextcloud.talk.newarch.local.db.TalkDatabase/1.json +++ b/app/schemas/com.nextcloud.talk.newarch.local.db.TalkDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "4e8c1ae6a440d8491937afe33a3ab085", + "identityHash": "4623fd40c40300731b8871e7d43e5f65", "entities": [ { "tableName": "conversations", @@ -212,7 +212,7 @@ }, { "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `conversation_id` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `actor_id` TEXT, `actor_type` TEXT, `actor_display_name` TEXT, `timestamp` INTEGER NOT NULL, `message` TEXT, `messageParameters` TEXT, `parent` TEXT, `replyable` INTEGER NOT NULL, `system_message_type` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`conversation_id`) REFERENCES `conversations`(`id`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `conversation_id` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `actor_id` TEXT, `actor_type` TEXT, `actor_display_name` TEXT, `timestamp` INTEGER NOT NULL, `message` TEXT, `messageParameters` TEXT, `parent` TEXT, `replyable` INTEGER NOT NULL, `system_message_type` TEXT, `reference_id` TEXT, `message_status` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`conversation_id`) REFERENCES `conversations`(`id`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", "fields": [ { "fieldPath": "id", @@ -285,6 +285,18 @@ "columnName": "system_message_type", "affinity": "TEXT", "notNull": false + }, + { + "fieldPath": "referenceId", + "columnName": "reference_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chatMessageStatus", + "columnName": "message_status", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { @@ -401,7 +413,7 @@ "views": [], "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, '4e8c1ae6a440d8491937afe33a3ab085')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4623fd40c40300731b8871e7d43e5f65')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java index 465f7adae..130c16c65 100644 --- a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java +++ b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java @@ -63,12 +63,21 @@ public class MentionAutocompleteCallback implements AutocompleteCallback { + actorType.equals("guests") || actorType.equals("bots") -> { var apiId: String? = sharedApplication!!.getString(R.string.nc_guest) if (!TextUtils.isEmpty(actorDisplayName)) { apiId = actorDisplayName diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.java b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.java index 0e9e1f90e..f5188d795 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.java @@ -31,10 +31,9 @@ public class ChatUtils { HashMap individualHashMap = messageParameters.get(key); if (individualHashMap.get("type").equals("user") || individualHashMap.get("type") .equals("guest") || individualHashMap.get("type").equals("call")) { - message = message.replaceAll("\\{" + key + "\\}", "@" + - messageParameters.get(key).get("name")); + message = message.replace("{" + key + "}", "@" + messageParameters.get(key).get("name")); } else if (individualHashMap.get("type").equals("file")) { - message = message.replaceAll("\\{" + key + "\\}", messageParameters.get(key).get("name")); + message = message.replace("{" + key + "}", messageParameters.get(key).get("name")); } } } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/ConversationsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/ConversationsRepositoryImpl.kt index 35b733a20..d54b206eb 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/ConversationsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/ConversationsRepositoryImpl.kt @@ -101,7 +101,7 @@ class ConversationsRepositoryImpl(val conversationsDao: ConversationsDao) : userId: Long, conversations: List, deleteOutdated: Boolean - ): List { + ) { val map = conversations.map { it.toConversationEntity() } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/MessagesRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/MessagesRepositoryImpl.kt index 4b9ece57c..89ee062e7 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/MessagesRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/MessagesRepositoryImpl.kt @@ -28,6 +28,9 @@ import androidx.lifecycle.map import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository import com.nextcloud.talk.newarch.local.dao.MessagesDao +import com.nextcloud.talk.newarch.local.models.User +import com.nextcloud.talk.newarch.local.models.hasSpreedFeatureCapability +import com.nextcloud.talk.newarch.local.models.other.ChatMessageStatus import com.nextcloud.talk.newarch.local.models.toChatMessage import com.nextcloud.talk.newarch.local.models.toMessageEntity @@ -42,6 +45,18 @@ class MessagesRepositoryImpl(private val messagesDao: MessagesDao) : MessagesRep } } + override fun getPendingMessagesForConversation(conversationId: String): LiveData> { + return messagesDao.getPendingMessagesLive(conversationId).distinctUntilChanged().map { + it.map { messageEntity -> + messageEntity.toChatMessage() + } + } + } + + override suspend fun getMessageForConversation(conversationId: String, messageId: Long): ChatMessage? { + return messagesDao.getMessageForConversation(conversationId, messageId)?.toChatMessage() + } + override fun getMessagesWithUserForConversationSince(conversationId: String, messageId: Long): LiveData> { return messagesDao.getMessagesWithUserForConversationSince(conversationId, messageId).distinctUntilChanged().map { it.map { messageEntity -> @@ -50,11 +65,23 @@ class MessagesRepositoryImpl(private val messagesDao: MessagesDao) : MessagesRep } } - override suspend fun saveMessagesForConversation(messages: List): List { + override suspend fun saveMessagesForConversation(user: User, messages: List, sendingMessages: Boolean){ + val shouldInsert = !user.hasSpreedFeatureCapability("chat-reference-id") || sendingMessages val updatedMessages = messages.map { + if (!user.hasSpreedFeatureCapability("chat-reference-id")) { + it.chatMessageStatus = ChatMessageStatus.RECEIVED + } it.toMessageEntity() } - return messagesDao.saveMessages(*updatedMessages.toTypedArray()) + if (shouldInsert) { + messagesDao.saveMessages(*updatedMessages.toTypedArray()) + } else { + messagesDao.updateMessages(user, updatedMessages.toTypedArray()) + } + } + + override suspend fun updateMessageStatus(status: Int, conversationId: String, messageId: Long) { + messagesDao.updateMessageStatus(status, conversationId, messageId) } } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/online/NextcloudTalkRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/online/NextcloudTalkRepositoryImpl.kt index 809e0f779..500cf0ea1 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/online/NextcloudTalkRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/online/NextcloudTalkRepositoryImpl.kt @@ -98,6 +98,11 @@ class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : Nextclou } } + override suspend fun sendChatMessage(user: User, conversationToken: String, message: CharSequence, authorDisplayName: String?, replyTo: Int?, referenceId: String?): Response { + return apiService.sendChatMessage(user.getCredentials(), ApiUtils.getUrlForChat(user.baseUrl, conversationToken), message, authorDisplayName, replyTo, referenceId) + } + + override suspend fun getChatMessagesForConversation(user: User, conversationToken: String, lookIntoFuture: Int, lastKnownMessageId: Int, includeLastKnown: Int): Response { val mutableMap = mutableMapOf() mutableMap["lookIntoFuture"] = lookIntoFuture diff --git a/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiService.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiService.kt index cf54aea30..051d7b819 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiService.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiService.kt @@ -39,6 +39,20 @@ import retrofit2.Response import retrofit2.http.* interface ApiService { + /* + Fieldmap items are as follows: + - "message": , + - "actorDisplayName" + */ + @FormUrlEncoded + @POST + suspend fun sendChatMessage(@Header("Authorization") authorization: String, + @Url url: String, + @Field("message") message: CharSequence, + @Field("actorDisplayName") actorDisplayName: String?, + @Field("replyTo") replyTo: Int?, + @Field("referenceId") referenceId: String?): Response + /* QueryMap items are as follows: - "lookIntoFuture": int (0 or 1), diff --git a/app/src/main/java/com/nextcloud/talk/newarch/di/module/ServiceModule.kt b/app/src/main/java/com/nextcloud/talk/newarch/di/module/ServiceModule.kt index e57825a87..2852ee087 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/di/module/ServiceModule.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/di/module/ServiceModule.kt @@ -23,25 +23,28 @@ package com.nextcloud.talk.newarch.di.module import android.content.Context +import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository +import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository import com.nextcloud.talk.newarch.domain.usecases.GetConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.JoinConversationUseCase import com.nextcloud.talk.newarch.services.GlobalService import com.nextcloud.talk.newarch.services.shortcuts.ShortcutService +import com.nextcloud.talk.newarch.utils.NetworkComponents import okhttp3.OkHttpClient import org.koin.dsl.module import java.net.CookieManager val ServiceModule = module { - single { createGlobalService(get(), get(), get(), get(), get(), get()) } + single { createGlobalService(get(), get(), get(), get(), get(), get(), get(), get(), get()) } single { createShortcutService(get(), get(), get()) } } fun createGlobalService(usersRepository: UsersRepository, cookieManager: CookieManager, - okHttpClient: OkHttpClient, conversationsRepository: ConversationsRepository, - getConversationUseCase: GetConversationUseCase, joinConversationUseCase: JoinConversationUseCase): GlobalService { - return GlobalService(usersRepository, cookieManager, okHttpClient, conversationsRepository, joinConversationUseCase, getConversationUseCase) + okHttpClient: OkHttpClient, apiErrorHandler: ApiErrorHandler, conversationsRepository: ConversationsRepository, + messagesRepository: MessagesRepository, networkComponents: NetworkComponents, getConversationUseCase: GetConversationUseCase, joinConversationUseCase: JoinConversationUseCase): GlobalService { + return GlobalService(usersRepository, cookieManager, okHttpClient, apiErrorHandler, conversationsRepository, messagesRepository, networkComponents, joinConversationUseCase, getConversationUseCase) } fun createShortcutService(context: Context, conversationsRepository: ConversationsRepository, conversationsService: GlobalService): ShortcutService { diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/di/module/UseCasesModule.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/di/module/UseCasesModule.kt index 50e7a2740..fd92bb199 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/domain/di/module/UseCasesModule.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/di/module/UseCasesModule.kt @@ -54,10 +54,15 @@ val UseCasesModule = module { factory { setConversationPasswordUseCase(get(), get()) } factory { getParticipantsForCallUseCase(get(), get()) } factory { createGetChatMessagesUseCase(get(), get()) } + factory { createSendChatMessageUseCase(get(), get()) } factory { getNotificationUseCase(get(), get()) } factory { createChatViewModelFactory(get(), get(), get(), get(), get(), get()) } } +fun createSendChatMessageUseCase(nextcloudTalkRepository: NextcloudTalkRepository, apiErrorHandler: ApiErrorHandler): SendChatMessageUseCase { + return SendChatMessageUseCase(nextcloudTalkRepository, apiErrorHandler) +} + fun createGetChatMessagesUseCase(nextcloudTalkRepository: NextcloudTalkRepository, apiErrorHandler: ApiErrorHandler): GetChatMessagesUseCase { return GetChatMessagesUseCase(nextcloudTalkRepository, apiErrorHandler) } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/ConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/ConversationsRepository.kt index c45cfb400..2a3038b34 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/ConversationsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/ConversationsRepository.kt @@ -35,7 +35,7 @@ interface ConversationsRepository { userId: Long, conversations: List, deleteOutdated: Boolean - ): List + ) suspend fun setChangingValueForConversation( userId: Long, diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/MessagesRepository.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/MessagesRepository.kt index 1e881dcd3..d159fa177 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/MessagesRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/MessagesRepository.kt @@ -24,9 +24,13 @@ package com.nextcloud.talk.newarch.domain.repository.offline import androidx.lifecycle.LiveData import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.newarch.local.models.User interface MessagesRepository { fun getMessagesWithUserForConversation(conversationId: String): LiveData> + fun getPendingMessagesForConversation(conversationId: String): LiveData> + suspend fun getMessageForConversation(conversationId: String, messageId: Long): ChatMessage? fun getMessagesWithUserForConversationSince(conversationId: String, messageId: Long): LiveData> - suspend fun saveMessagesForConversation(messages: List): List + suspend fun saveMessagesForConversation(user: User, messages: List, sendingMessages: Boolean) + suspend fun updateMessageStatus(status: Int, conversationId: String, messageId: Long) } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/online/NextcloudTalkRepository.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/online/NextcloudTalkRepository.kt index c4df6c747..2a88d16ba 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/online/NextcloudTalkRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/online/NextcloudTalkRepository.kt @@ -39,6 +39,7 @@ import com.nextcloud.talk.newarch.local.models.UserNgEntity import retrofit2.Response interface NextcloudTalkRepository { + suspend fun sendChatMessage(user: User, conversationToken: String, message: CharSequence, authorDisplayName: String?, replyTo: Int?, referenceId: String?): Response suspend fun getChatMessagesForConversation(user: User, conversationToken: String, lookIntoFuture: Int, lastKnownMessageId: Int, includeLastKnown: Int = 0): Response suspend fun getNotificationForUser(user: UserNgEntity, notificationId: String): NotificationOverall suspend fun getParticipantsForCall(user: UserNgEntity, conversationToken: String): ParticipantsOverall diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/SendChatMessageUseCase.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/SendChatMessageUseCase.kt new file mode 100644 index 000000000..f5a8fe17c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/SendChatMessageUseCase.kt @@ -0,0 +1,20 @@ +package com.nextcloud.talk.newarch.domain.usecases + +import com.nextcloud.talk.models.json.chat.ChatOverall +import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler +import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkRepository +import com.nextcloud.talk.newarch.domain.usecases.base.UseCase +import com.nextcloud.talk.newarch.local.models.User +import org.koin.core.parameter.DefinitionParameters +import retrofit2.Response + +class SendChatMessageUseCase constructor( + private val nextcloudTalkRepository: NextcloudTalkRepository, + apiErrorHandler: ApiErrorHandler? +) : UseCase, Any?>(apiErrorHandler) { + override suspend fun run(params: Any?): Response { + val definitionParameters = params as DefinitionParameters + val user: User = definitionParameters[0] + return nextcloudTalkRepository.sendChatMessage(definitionParameters[0], definitionParameters[1], definitionParameters[2], user.displayName, definitionParameters[3], definitionParameters[4]) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/account/loginentry/LoginEntryViewModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/account/loginentry/LoginEntryViewModel.kt index 44f1b0be4..ce30488fe 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/account/loginentry/LoginEntryViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/account/loginentry/LoginEntryViewModel.kt @@ -33,7 +33,7 @@ import com.nextcloud.talk.models.json.push.PushConfigurationStateWrapper import com.nextcloud.talk.models.json.push.PushRegistrationOverall import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall import com.nextcloud.talk.models.json.userprofile.UserProfileOverall -import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel +import com.nextcloud.talk.newarch.mvvm.BaseViewModel import com.nextcloud.talk.newarch.data.model.ErrorModel import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository import com.nextcloud.talk.newarch.domain.usecases.* diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/account/serverentry/ServerEntryViewModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/account/serverentry/ServerEntryViewModel.kt index 26b2fcc76..f85e39dcc 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/account/serverentry/ServerEntryViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/account/serverentry/ServerEntryViewModel.kt @@ -26,7 +26,7 @@ import android.app.Application import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall -import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel +import com.nextcloud.talk.newarch.mvvm.BaseViewModel import com.nextcloud.talk.newarch.data.model.ErrorModel import com.nextcloud.talk.newarch.domain.usecases.GetCapabilitiesUseCase import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatPresenter.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatPresenter.kt index f6c79f31b..18d94b8a0 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatPresenter.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatPresenter.kt @@ -13,6 +13,7 @@ import com.amulyakhare.textdrawable.TextDrawable import com.nextcloud.talk.R import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.newarch.features.chat.interfaces.ImageLoaderInterface +import com.nextcloud.talk.newarch.local.models.other.ChatMessageStatus import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType import com.nextcloud.talk.utils.TextMatchers @@ -80,6 +81,8 @@ open class ChatPresenter(context: Context, private val onElementClickPa } holder.itemView.messageTime?.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME) + holder.itemView.sendingProgressBar.isVisible = it.chatMessageStatus != ChatMessageStatus.RECEIVED + holder.itemView.failedToSendNotice.isVisible = it.chatMessageStatus == ChatMessageStatus.FAILED holder.itemView.chatMessage.text = it.text if (TextMatchers.isMessageWithSingleEmoticonOnly(it.text)) { holder.itemView.chatMessage.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24f) diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt index 6ef4125f4..d1b559e7e 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt @@ -93,7 +93,6 @@ import com.stfalcon.chatkit.utils.DateFormatter import com.uber.autodispose.lifecycle.LifecycleScopeProvider import com.vanniktech.emoji.EmojiPopup import kotlinx.android.synthetic.main.controller_chat.view.* -import kotlinx.android.synthetic.main.conversations_list_view.view.* import kotlinx.android.synthetic.main.item_message_quote.view.* import kotlinx.android.synthetic.main.lobby_view.view.* import kotlinx.android.synthetic.main.view_message_input.view.* @@ -105,7 +104,6 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface { override val scopeProvider: LifecycleScopeProvider<*> = ControllerScopeProvider.from(this) override val lifecycleOwner = ControllerLifecycleOwner(this) - private lateinit var viewModel: ChatViewModel val factory: ChatViewModelFactory by inject() private val networkComponents: NetworkComponents by inject() @@ -211,12 +209,6 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface { conversation.observe(this@ChatView) { conversation -> setTitle() - if (Conversation.ConversationType.ONE_TO_ONE_CONVERSATION == conversation?.type) { - loadAvatar() - } else { - actionBar?.setIcon(null) - } - shouldShowLobby = conversation!!.shouldShowLobby(user) isReadOnlyConversation = conversation.conversationReadOnlyState == Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY @@ -375,6 +367,7 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface { private fun hideReplyView() { view?.messageInputView?.let { with (it) { + quotedMessageLayout.tag = null quotedMessageLayout.isVisible = false attachmentButton.isVisible = true attachmentButtonSpace.isVisible = true @@ -393,7 +386,6 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface { quotedChatText.text = chatMessage.text quotedAuthor.text = chatMessage.user.name quotedMessageTime.text = DateFormatter.format(chatMessage.createdAt, DateFormatter.Template.TIME) - loadImage(quotedUserAvatar, chatMessage.user.avatar) chatMessage.imageUrl?.let { previewImageUrl -> @@ -493,10 +485,6 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface { conversationVoiceCallMenuItem?.isVisible = true conversationVideoMenuItem?.isVisible = true } - - if (Conversation.ConversationType.ONE_TO_ONE_CONVERSATION == viewModel.conversation.value?.type) { - loadAvatar() - } } private fun setupViews() { @@ -622,25 +610,12 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface { private fun submitMessage() { val editable = view?.messageInput?.editableText editable?.let { - val mentionSpans = it.getSpans( - 0, it.length, - Spans.MentionChipSpan::class.java - ) - var mentionSpan: Spans.MentionChipSpan - for (i in mentionSpans.indices) { - mentionSpan = mentionSpans[i] - var mentionId = mentionSpan.id - if (mentionId.contains(" ") || mentionId.startsWith("guest/")) { - mentionId = "\"" + mentionId + "\"" - } - it.replace( - it.getSpanStart(mentionSpan), it.getSpanEnd(mentionSpan), "@$mentionId" - ) - } - + val replyMessageId= view?.messageInputView?.quotedMessageLayout?.tag as Long? view?.messageInput?.setText("") - viewModel.sendMessage(it) - + viewModel.sendMessage(it, replyMessageId) + if (replyMessageId != null) { + hideReplyView() + } } } @@ -696,37 +671,6 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface { } } - private fun loadAvatar() { - val imageLoader = networkComponents.getImageLoader(viewModel.user) - conversationVoiceCallMenuItem?.let { - val avatarSize = DisplayUtils.convertDpToPixel( - it.icon!!.intrinsicWidth.toFloat(), activity!! - ) - .toInt() - - avatarSize.let { - val target = object : Target { - override fun onSuccess(result: Drawable) { - super.onSuccess(result) - actionBar?.setIcon(result) - } - } - - viewModel.conversation.value?.let { - val avatarRequest = Images().getRequestForUrl( - imageLoader, context, ApiUtils.getUrlForAvatarWithNameAndPixels( - viewModel.user.baseUrl, - it.name, avatarSize / 2 - ), viewModel.user, target, this, - CircleCropTransformation() - ) - imageLoader.load(avatarRequest) - } - } - - } - } - override fun getLayoutId(): Int { return R.layout.controller_chat } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewLiveDataSource.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewLiveDataSource.kt index 11f888de6..8c8f5d12d 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewLiveDataSource.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewLiveDataSource.kt @@ -33,7 +33,7 @@ class ChatViewLiveDataSource(private val data: LiveData } if (first.data is ChatMessage && second.data is ChatMessage) { - return first.data.jsonMessageId == second.data.jsonMessageId + return first.data.jsonMessageId == second.data.jsonMessageId || first.data.referenceId == second.data.referenceId } return false diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModel.kt index 0727fd068..dea7b9dd1 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModel.kt @@ -23,7 +23,7 @@ package com.nextcloud.talk.newarch.features.chat import android.app.Application -import android.text.TextUtils +import android.text.Editable import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.lifecycle.map @@ -32,23 +32,31 @@ import com.bluelinelabs.conductor.Controller import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.chat.ChatOverall import com.nextcloud.talk.models.json.conversations.Conversation -import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel import com.nextcloud.talk.newarch.data.model.ErrorModel import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository import com.nextcloud.talk.newarch.domain.usecases.GetChatMessagesUseCase +import com.nextcloud.talk.newarch.domain.usecases.SendChatMessageUseCase import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse -import com.nextcloud.talk.newarch.local.models.User -import com.nextcloud.talk.newarch.local.models.UserNgEntity -import com.nextcloud.talk.newarch.local.models.toUser -import com.nextcloud.talk.newarch.local.models.toUserEntity +import com.nextcloud.talk.newarch.local.models.* +import com.nextcloud.talk.newarch.local.models.other.ChatMessageStatus +import com.nextcloud.talk.newarch.mvvm.BaseViewModel import com.nextcloud.talk.newarch.services.GlobalService import com.nextcloud.talk.newarch.services.GlobalServiceInterface import com.nextcloud.talk.newarch.utils.NetworkComponents +import com.nextcloud.talk.newarch.utils.hashWithAlgorithm +import com.nextcloud.talk.utils.text.Spans import kotlinx.coroutines.launch import org.koin.core.parameter.parametersOf import retrofit2.Response +import kotlin.collections.HashMap +import kotlin.collections.hashMapOf +import kotlin.collections.indices +import kotlin.collections.listOf +import kotlin.collections.map +import kotlin.collections.mutableListOf +import kotlin.collections.set class ChatViewModel constructor(application: Application, private val networkComponents: NetworkComponents, @@ -62,7 +70,7 @@ class ChatViewModel constructor(application: Application, val futureStartingPoint: MutableLiveData = MutableLiveData() private var initConversation: Conversation? = null - val messagesLiveData = Transformations.switchMap(futureStartingPoint) {futureStartingPoint -> + val messagesLiveData = Transformations.switchMap(futureStartingPoint) { futureStartingPoint -> conversation.value?.let { messagesRepository.getMessagesWithUserForConversationSince(it.databaseId!!, futureStartingPoint).map { chatMessagesList -> chatMessagesList.map { chatMessage -> @@ -92,8 +100,75 @@ class ChatViewModel constructor(application: Application, } } - fun sendMessage(message: CharSequence) { + fun sendMessage(editable: Editable, replyTo: Long?) { + val messageParameters = hashMapOf>() + val mentionSpans = editable.getSpans( + 0, editable.length, + Spans.MentionChipSpan::class.java + ) + var mentionSpan: Spans.MentionChipSpan + val ids = mutableListOf() + for (i in mentionSpans.indices) { + mentionSpan = mentionSpans[i] + var mentionId = mentionSpan.id + if (mentionId.contains(" ") || mentionId.startsWith("guest/")) { + mentionId = "\"" + mentionId + "\"" + } + val mentionNo = if (ids.contains("mentionId")) ids.indexOf("mentionId") + 1 else ids.size + 1 + val mentionReplace = "mention-${mentionSpan.type}$mentionNo" + if (!ids.contains(mentionId)) { + ids.add(mentionId) + messageParameters[mentionReplace] = hashMapOf("type" to mentionSpan.type, "id" to mentionId.toString(), "name" to mentionSpan.label.toString()) + } + + val start = editable.getSpanStart(mentionSpan) + editable.replace(start, editable.getSpanEnd(mentionSpan), "") + editable.insert(start, "{$mentionReplace}") + } + + if (user.hasSpreedFeatureCapability("chat-reference-id")) { + ioScope.launch { + val chatMessage = ChatMessage() + val timestamp = System.currentTimeMillis() + val sha1 = timestamp.toString().hashWithAlgorithm("SHA-1") + conversation.value?.databaseId?.let { conversationDatabaseId -> + chatMessage.internalMessageId = sha1 + chatMessage.internalConversationId = conversationDatabaseId + chatMessage.timestamp = timestamp / 1000 + chatMessage.referenceId = sha1 + chatMessage.replyable = false + // can also be "guests", but not now + chatMessage.actorId = user.userId + chatMessage.actorType = "users" + chatMessage.actorDisplayName = user.displayName + chatMessage.message = editable.toString() + chatMessage.systemMessageType = null + chatMessage.chatMessageStatus = ChatMessageStatus.PENDING_MESSAGE_SEND + if (replyTo != null) { + chatMessage.parentMessage = messagesRepository.getMessageForConversation(conversationDatabaseId, replyTo) + } else { + chatMessage.parentMessage = null + } + chatMessage.messageParameters = messageParameters + messagesRepository.saveMessagesForConversation(user, listOf(chatMessage), true) + } + } + } else { + val sendChatMessageUseCase = SendChatMessageUseCase(networkComponents.getRepository(false, user), apiErrorHandler) + // No reference id needed here + initConversation?.let { + sendChatMessageUseCase.invoke(viewModelScope, parametersOf(user, it.token, editable, replyTo, null), object : UseCaseResponse> { + override suspend fun onSuccess(result: Response) { + // also do nothing, we did it - time to celebrate1 + } + + override suspend fun onError(errorModel: ErrorModel?) { + // Do nothing, error - tough luck + } + }) + } + } } override suspend fun gotConversationInfoForUser(userNgEntity: UserNgEntity, conversation: Conversation?, operationStatus: GlobalServiceInterface.OperationStatus) { @@ -124,11 +199,10 @@ class ChatViewModel constructor(application: Application, val messages = result.body()?.ocs?.data messages?.let { for (message in it) { - message.activeUser = userNgEntity message.internalConversationId = conversation.databaseId } - messagesRepository.saveMessagesForConversation(it) + messagesRepository.saveMessagesForConversation(user, it, false) } val xChatLastGivenHeader: String? = result.headers().get("X-Chat-Last-Given") @@ -159,25 +233,19 @@ class ChatViewModel constructor(application: Application, val messages = result.body()?.ocs?.data messages?.let { for (message in it) { - message.activeUser = userNgEntity message.internalConversationId = conversation.databaseId } - messagesRepository.saveMessagesForConversation(it) + messagesRepository.saveMessagesForConversation(user, it, false) } - if (result.code() == 200) { val xChatLastGivenHeader: String? = result.headers().get("X-Chat-Last-Given") if (xChatLastGivenHeader != null) { pullFutureMessagesForUserAndConversation(userNgEntity, conversation, xChatLastGivenHeader.toInt()) } - } else { - pullFutureMessagesForUserAndConversation(userNgEntity, conversation, lastKnownMessageId) - } } override suspend fun onError(errorModel: ErrorModel?) { - pullFutureMessagesForUserAndConversation(userNgEntity, conversation) } }) } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsViewModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsViewModel.kt index b8f31e141..c056ef3b8 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsViewModel.kt @@ -32,7 +32,7 @@ import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.conversations.ConversationOverall import com.nextcloud.talk.models.json.participants.AddParticipantOverall import com.nextcloud.talk.models.json.participants.Participant -import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel +import com.nextcloud.talk.newarch.mvvm.BaseViewModel import com.nextcloud.talk.newarch.data.model.ErrorModel import com.nextcloud.talk.newarch.domain.usecases.AddParticipantToConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.CreateConversationUseCase diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/groupconversation/GroupConversationViewModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/groupconversation/GroupConversationViewModel.kt index 8506d3507..d575e20ae 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/groupconversation/GroupConversationViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/groupconversation/GroupConversationViewModel.kt @@ -29,7 +29,7 @@ import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.viewModelScope import com.nextcloud.talk.models.json.conversations.ConversationOverall import com.nextcloud.talk.models.json.generic.GenericOverall -import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel +import com.nextcloud.talk.newarch.mvvm.BaseViewModel import com.nextcloud.talk.newarch.data.model.ErrorModel import com.nextcloud.talk.newarch.domain.usecases.CreateConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.SetConversationPasswordUseCase diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt index 2676fee51..e2c1963db 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt @@ -34,7 +34,7 @@ import coil.transform.CircleCropTransformation import com.nextcloud.talk.R import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.generic.GenericOverall -import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel +import com.nextcloud.talk.newarch.mvvm.BaseViewModel import com.nextcloud.talk.newarch.data.model.ErrorModel import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository import com.nextcloud.talk.newarch.domain.usecases.DeleteConversationUseCase @@ -200,9 +200,6 @@ class ConversationsListViewModel ( networkStateLiveData.postValue(ConversationsListViewNetworkState.LOADED) val mutableList = result.toMutableList() val internalUserId = globalService.currentUserLiveData.value!!.id - mutableList.forEach { - it.databaseUserId = internalUserId - } conversationsRepository.saveConversationsForUser( internalUserId, diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/converters/ChatMessageStatusConverter.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/converters/ChatMessageStatusConverter.kt new file mode 100644 index 000000000..b27d7884a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/converters/ChatMessageStatusConverter.kt @@ -0,0 +1,45 @@ +/* + * + * * Nextcloud Talk application + * * + * * @author Mario Danic + * * Copyright (C) 2017-2020 Mario Danic + * * + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * at your option) any later version. + * * + * * This program is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU General Public License for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see . + * + */ + +package com.nextcloud.talk.newarch.local.converters + +import androidx.room.TypeConverter +import com.nextcloud.talk.newarch.local.models.other.ChatMessageStatus + +class ChatMessageStatusConverter { + @TypeConverter + fun fromChatMessageStatusToInt(chatMessageStatus: ChatMessageStatus): Int { + return chatMessageStatus.ordinal + } + + @TypeConverter + fun fromIntToChatMessageStatus(value: Int): ChatMessageStatus { + return when (value) { + 0 -> ChatMessageStatus.SENT + 1 -> ChatMessageStatus.RECEIVED + 2 -> ChatMessageStatus.PENDING_MESSAGE_SEND + 3 -> ChatMessageStatus.PENDING_FILE_UPLOAD + 4 -> ChatMessageStatus.PENDING_FILE_SHARE + else -> ChatMessageStatus.FAILED + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/dao/ConversationsDao.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/dao/ConversationsDao.kt index 09413444f..ce7c574a7 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/dao/ConversationsDao.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/dao/ConversationsDao.kt @@ -41,11 +41,11 @@ abstract class ConversationsDao { @Query("DELETE FROM conversations WHERE user_id = :userId") abstract suspend fun clearConversationsForUser(userId: Long) - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun saveConversationWithInsert(conversation: ConversationEntity): Long + @Update(onConflict = OnConflictStrategy.IGNORE) + abstract suspend fun update(conversation: ConversationEntity): Int @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun saveConversationsWithInsert(vararg conversations: ConversationEntity): List + abstract suspend fun insert(conversation: ConversationEntity) @Query( "UPDATE conversations SET changing = :changing WHERE user_id = :userId AND conversation_id = :conversationId" @@ -88,18 +88,26 @@ abstract class ConversationsDao { userId: Long, newConversations: Array, deleteOutdated: Boolean - ): List { + ) { val timestamp = System.currentTimeMillis() val conversationsWithTimestampApplied = newConversations.map { it.modifiedAt = timestamp + it.userId = userId + it.id = it.userId.toString() + "@" + it.token it } - val list = saveConversationsWithInsert(*conversationsWithTimestampApplied.toTypedArray()) + conversationsWithTimestampApplied.forEach { internalUpsert(it) } if (deleteOutdated) { deleteConversationsForUserWithTimestamp(userId, timestamp) } - return list + } + + private suspend fun internalUpsert(conversationEntity: ConversationEntity) { + val count = update(conversationEntity) + if (count == 0) { + insert(conversationEntity) + } } } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/dao/MessagesDao.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/dao/MessagesDao.kt index cf85cf1fa..aeadfb866 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/dao/MessagesDao.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/dao/MessagesDao.kt @@ -23,21 +23,76 @@ package com.nextcloud.talk.newarch.local.dao import androidx.lifecycle.LiveData -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.* +import com.nextcloud.talk.newarch.local.models.ConversationEntity import com.nextcloud.talk.newarch.local.models.MessageEntity +import com.nextcloud.talk.newarch.local.models.User @Dao abstract class MessagesDao { - @Query("SELECT * FROM messages WHERE conversation_id = :conversationId ORDER BY message_id ASC") + @Query("SELECT * FROM messages WHERE conversation_id = :conversationId ORDER BY timestamp ASC") abstract fun getMessagesWithUserForConversation(conversationId: String): LiveData> @Insert(onConflict = OnConflictStrategy.REPLACE) abstract suspend fun saveMessages(vararg messages: MessageEntity): List - @Query("SELECT * FROM messages WHERE conversation_id = :conversationId AND message_id >= :messageId ORDER BY message_id ASC") + @Query("SELECT * FROM messages WHERE conversation_id = :conversationId AND id = reference_id") + abstract suspend fun getPendingMessages(conversationId: String): List + + @Query("SELECT * FROM messages WHERE conversation_id = :conversationId and id = reference_id and message_status != 5 and message_status != 0") + abstract fun getPendingMessagesLive(conversationId: String): LiveData> + + @Query( + "UPDATE messages SET id = :newId WHERE conversation_id = :conversationId AND reference_id = :referenceId" + ) + abstract suspend fun updateMessageId(newId: String, conversationId: String, referenceId: String) + + @Query("SELECT * FROM messages WHERE conversation_id = :conversationId AND (message_id >= :messageId OR message_id = 0) ORDER BY timestamp ASC") abstract fun getMessagesWithUserForConversationSince(conversationId: String, messageId: Long): LiveData> + + @Query("SELECT * FROM messages WHERE conversation_id = :conversationId AND message_id = :messageId") + abstract fun getMessageForConversation(conversationId: String, messageId: Long): MessageEntity? + + @Query( + "UPDATE messages SET message_status = :status WHERE conversation_id = :conversationId AND message_id = :messageId" + ) + abstract suspend fun updateMessageStatus( + status: Int, + conversationId: String, + messageId: Long + ) + + @Update(onConflict = OnConflictStrategy.IGNORE) + abstract suspend fun update(message: MessageEntity): Int + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract suspend fun insert(message: MessageEntity) + + @Transaction + open suspend fun updateMessages(user: User, messages: Array) { + val messagesToUpdate = messages.toMutableList() + if (messagesToUpdate.size > 0) { + val conversationId = messagesToUpdate[0].conversationId + val pendingMessages = getPendingMessages(conversationId) + val pendingMessagesReferenceIds = pendingMessages.map { it.referenceId } + messagesToUpdate.forEach { + it.referenceId?.let { referenceId -> + if (pendingMessagesReferenceIds.contains(referenceId)) { + updateMessageId(it.id, it.conversationId, referenceId) + } + } + } + + messagesToUpdate.forEach { internalUpsert(it) } + } + } + + private suspend fun internalUpsert(message: MessageEntity) { + val count = update(message) + if (count == 0) { + insert(message) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/db/TalkDatabase.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/db/TalkDatabase.kt index 476655201..7b94daa86 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/db/TalkDatabase.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/db/TalkDatabase.kt @@ -27,6 +27,8 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteQuery import com.nextcloud.talk.newarch.local.converters.* import com.nextcloud.talk.newarch.local.dao.ConversationsDao import com.nextcloud.talk.newarch.local.dao.MessagesDao @@ -48,7 +50,7 @@ import org.parceler.converter.HashMapParcelConverter PushConfigurationConverter::class, CapabilitiesConverter::class, SignalingSettingsConverter::class, UserStatusConverter::class, SystemMessageTypeConverter::class, ParticipantMapConverter::class, - HashMapHashMapConverter::class + HashMapHashMapConverter::class, ChatMessageStatusConverter::class ) abstract class TalkDatabase : RoomDatabase() { @@ -71,6 +73,12 @@ abstract class TalkDatabase : RoomDatabase() { private fun build(context: Context) = Room.databaseBuilder(context.applicationContext, TalkDatabase::class.java, DB_NAME) .fallbackToDestructiveMigration() + .addCallback(object : RoomDatabase.Callback() { + override fun onOpen(db: SupportSQLiteDatabase) { + super.onOpen(db) + db.execSQL("PRAGMA defer_foreign_keys = 1") + } + }) .build() } } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/models/MessageEntity.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/models/MessageEntity.kt index c43fceca5..652d0b043 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/models/MessageEntity.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/models/MessageEntity.kt @@ -26,17 +26,18 @@ import androidx.room.* import androidx.room.ForeignKey.CASCADE import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType +import com.nextcloud.talk.newarch.local.models.other.ChatMessageStatus @Entity( tableName = "messages", indices = [Index(value = ["conversation_id"])], foreignKeys = [ForeignKey( entity = ConversationEntity::class, + deferred = true, parentColumns = arrayOf("id"), childColumns = arrayOf("conversation_id"), onDelete = CASCADE, - onUpdate = CASCADE, - deferred = true + onUpdate = CASCADE )] ) data class MessageEntity( @@ -51,7 +52,9 @@ data class MessageEntity( @ColumnInfo(name = "messageParameters") var messageParameters: HashMap>? = null, @ColumnInfo(name = "parent") var parentMessage: ChatMessage? = null, @ColumnInfo(name = "replyable") var replyable: Boolean = false, - @ColumnInfo(name = "system_message_type") var systemMessageType: SystemMessageType? = null + @ColumnInfo(name = "system_message_type") var systemMessageType: SystemMessageType? = null, + @ColumnInfo(name = "reference_id") var referenceId: String? = null, + @ColumnInfo(name = "message_status") var chatMessageStatus: ChatMessageStatus = ChatMessageStatus.RECEIVED ) fun MessageEntity.toChatMessage(): ChatMessage { @@ -68,12 +71,15 @@ fun MessageEntity.toChatMessage(): ChatMessage { chatMessage.systemMessageType = this.systemMessageType chatMessage.replyable = this.replyable chatMessage.parentMessage = this.parentMessage + chatMessage.referenceId = this.referenceId + chatMessage.chatMessageStatus = this.chatMessageStatus return chatMessage } fun ChatMessage.toMessageEntity(): MessageEntity { - val messageEntity = MessageEntity(this.internalConversationId + "@" + this.jsonMessageId, this.internalConversationId!!) - messageEntity.messageId = this.jsonMessageId!! + val messageEntityId = if (this.internalMessageId != null) internalMessageId else this.internalConversationId + "@" + this.jsonMessageId + val messageEntity = MessageEntity(messageEntityId!!, this.internalConversationId!!) + messageEntity.messageId = this.jsonMessageId ?: 0 messageEntity.actorType = this.actorType messageEntity.actorId = this.actorId messageEntity.actorDisplayName = this.actorDisplayName @@ -83,6 +89,7 @@ fun ChatMessage.toMessageEntity(): MessageEntity { messageEntity.replyable = this.replyable messageEntity.messageParameters = this.messageParameters messageEntity.parentMessage = this.parentMessage - + messageEntity.referenceId = this.referenceId + messageEntity.chatMessageStatus = this.chatMessageStatus return messageEntity } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/models/other/ChatMessageStatus.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/models/other/ChatMessageStatus.kt new file mode 100644 index 000000000..2ec337207 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/models/other/ChatMessageStatus.kt @@ -0,0 +1,10 @@ +package com.nextcloud.talk.newarch.local.models.other + +enum class ChatMessageStatus { + SENT, + RECEIVED, + PENDING_MESSAGE_SEND, + PENDING_FILE_UPLOAD, + PENDING_FILE_SHARE, + FAILED +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/mvvm/BaseViewModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/mvvm/BaseViewModel.kt index d38d8471d..d1b22ed1a 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/mvvm/BaseViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/mvvm/BaseViewModel.kt @@ -20,7 +20,7 @@ * */ -package com.nextcloud.talk.newarch.conversationsList.mvp +package com.nextcloud.talk.newarch.mvvm import android.app.Application import android.content.Context diff --git a/app/src/main/java/com/nextcloud/talk/newarch/services/GlobalService.kt b/app/src/main/java/com/nextcloud/talk/newarch/services/GlobalService.kt index c3e3d112e..37d446729 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/services/GlobalService.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/services/GlobalService.kt @@ -23,39 +23,106 @@ package com.nextcloud.talk.newarch.services import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.models.json.chat.ChatOverall import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.conversations.ConversationOverall import com.nextcloud.talk.newarch.data.model.ErrorModel +import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository +import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository import com.nextcloud.talk.newarch.domain.usecases.GetConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.JoinConversationUseCase +import com.nextcloud.talk.newarch.domain.usecases.SendChatMessageUseCase import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.newarch.local.models.other.ChatMessageStatus +import com.nextcloud.talk.newarch.local.models.toUser +import com.nextcloud.talk.newarch.utils.NetworkComponents import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import okhttp3.OkHttpClient import org.koin.core.KoinComponent import org.koin.core.parameter.parametersOf +import retrofit2.Response import java.net.CookieManager +import java.util.concurrent.ConcurrentHashMap class GlobalService constructor(usersRepository: UsersRepository, cookieManager: CookieManager, - okHttpClient: OkHttpClient, + private val okHttpClient: OkHttpClient, + private val apiErrorHandler: ApiErrorHandler, private val conversationsRepository: ConversationsRepository, + private val messagesRepository: MessagesRepository, + private val networkComponents: NetworkComponents, private val joinConversationUseCase: JoinConversationUseCase, private val getConversationUseCase: GetConversationUseCase) : KoinComponent { private val applicationScope = CoroutineScope(Dispatchers.Default) private val previousUser: UserNgEntity? = null val currentUserLiveData: LiveData = usersRepository.getActiveUserLiveData() - private var currentConversation: Conversation? = null + private var currentConversation: MutableLiveData = MutableLiveData(null) + private val pendingMessages: LiveData> = Transformations.switchMap(currentConversation) { conversation -> + conversation?.let { + messagesRepository.getPendingMessagesForConversation(it.databaseId!!) + } + } + + private var messagesOperations: ConcurrentHashMap> = ConcurrentHashMap>() init { + pendingMessages.observeForever { chatMessages -> + for (chatMessage in chatMessages) { + if (!messagesOperations.contains(chatMessage.internalMessageId) || messagesOperations[chatMessage.internalMessageId]?.first != chatMessage) { + messagesOperations[chatMessage.internalMessageId!!] = Pair(chatMessage, 0) + applicationScope.launch { + sendMessage(chatMessage) + } + } + } + } + currentUserLiveData.observeForever { user -> user?.let { if (it.id != previousUser?.id) { cookieManager.cookieStore.removeAll() - currentConversation = null + currentConversation.postValue(null) + } + } + } + } + + suspend fun sendMessage(chatMessage: ChatMessage) { + val currentUser = currentUserLiveData.value?.toUser() + val conversation = currentConversation.value + val operationChatMessage = messagesOperations[chatMessage.internalMessageId] + + operationChatMessage?.let { pair -> + conversation?.let { conversation -> + if (pair.second == 4) { + messagesOperations.remove(pair.first.internalMessageId) + messagesRepository.updateMessageStatus(ChatMessageStatus.FAILED.ordinal, conversation.databaseId!!, pair.first.jsonMessageId!!) + } else { + currentUser?.let { user -> + if (chatMessage.internalConversationId == conversation.databaseId && conversation.databaseUserId == currentUser.id) { + val sendChatMessageUseCase = SendChatMessageUseCase(networkComponents.getRepository(false, user), apiErrorHandler) + sendChatMessageUseCase.invoke(applicationScope, parametersOf(user, conversation.token, chatMessage.message, chatMessage.parentMessage?.jsonMessageId, chatMessage.referenceId), object : UseCaseResponse> { + override suspend fun onSuccess(result: Response) { + messagesOperations.remove(pair.first.internalMessageId!!) + messagesRepository.updateMessageStatus(ChatMessageStatus.SENT.ordinal, conversation.databaseId!!, pair.first.jsonMessageId!!) + } + + override suspend fun onError(errorModel: ErrorModel?) { + val newValue = operationChatMessage.second + 1 + messagesOperations[pair.first.internalMessageId!!] = Pair(chatMessage, newValue) + sendMessage(chatMessage) + } + }) + } + } } } } @@ -63,6 +130,7 @@ class GlobalService constructor(usersRepository: UsersRepository, suspend fun getConversation(conversationToken: String, globalServiceInterface: GlobalServiceInterface) { val currentUser = currentUserLiveData.value + val getConversationUseCase = GetConversationUseCase(networkComponents.getRepository(true, currentUser!!.toUser()), apiErrorHandler) getConversationUseCase.invoke(applicationScope, parametersOf( currentUser, conversationToken @@ -72,6 +140,7 @@ class GlobalService constructor(usersRepository: UsersRepository, currentUser?.let { conversationsRepository.saveConversationsForUser(it.id, listOf(result.ocs.data), false) globalServiceInterface.gotConversationInfoForUser(it, result.ocs.data, GlobalServiceInterface.OperationStatus.STATUS_OK) + } } @@ -80,11 +149,13 @@ class GlobalService constructor(usersRepository: UsersRepository, globalServiceInterface.gotConversationInfoForUser(it, null, GlobalServiceInterface.OperationStatus.STATUS_FAILED) } } + }) } suspend fun joinConversation(conversationToken: String, conversationPassword: String?, globalServiceInterface: GlobalServiceInterface) { val currentUser = currentUserLiveData.value + val joinConversationUseCase = JoinConversationUseCase(networkComponents.getRepository(true, currentUser!!.toUser()), apiErrorHandler) joinConversationUseCase.invoke(applicationScope, parametersOf( currentUser, conversationToken, @@ -94,14 +165,14 @@ class GlobalService constructor(usersRepository: UsersRepository, override suspend fun onSuccess(result: ConversationOverall) { currentUser?.let { conversationsRepository.saveConversationsForUser(it.id, listOf(result.ocs.data), false) - currentConversation = conversationsRepository.getConversationForUserWithToken(it.id, result.ocs!!.data!!.token!!) - globalServiceInterface.joinedConversationForUser(it, currentConversation, GlobalServiceInterface.OperationStatus.STATUS_OK) + currentConversation.postValue(conversationsRepository.getConversationForUserWithToken(it.id, result.ocs!!.data!!.token!!)) + globalServiceInterface.joinedConversationForUser(it, currentConversation.value, GlobalServiceInterface.OperationStatus.STATUS_OK) } } override suspend fun onError(errorModel: ErrorModel?) { currentUser?.let { - globalServiceInterface.joinedConversationForUser(it, currentConversation, GlobalServiceInterface.OperationStatus.STATUS_FAILED) + globalServiceInterface.joinedConversationForUser(it, currentConversation.value, GlobalServiceInterface.OperationStatus.STATUS_FAILED) } } }) diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt index 3469b0e48..f99686e85 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt @@ -291,15 +291,15 @@ object DisplayUtils { val start = stringText.indexOf(m.group(), lastStartIndex) val end = start + m.group().length lastStartIndex = end - mentionChipSpan = Spans.MentionChipSpan( + /*mentionChipSpan = Spans.MentionChipSpan( getDrawableForMentionChipSpan( context, - id, label, conversationUser, type, chipXmlRes, null + id, label, conversationUser, chipXmlRes, null ), BetterImageSpan.ALIGN_CENTER, id, label - ) - spannableString.setSpan(mentionChipSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + )*/ + //spannableString.setSpan(mentionChipSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) if ("user" == type && conversationUser.userId != id) { spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) } diff --git a/app/src/main/java/com/nextcloud/talk/utils/text/Spans.java b/app/src/main/java/com/nextcloud/talk/utils/text/Spans.java index 093cb16e4..ae4c66a4c 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/text/Spans.java +++ b/app/src/main/java/com/nextcloud/talk/utils/text/Spans.java @@ -34,12 +34,14 @@ public class Spans { public static class MentionChipSpan extends BetterImageSpan { public String id; public CharSequence label; + public String type; public MentionChipSpan(@NonNull Drawable drawable, int verticalAlignment, String id, - CharSequence label) { + CharSequence label, String type) { super(drawable, verticalAlignment); this.id = id; this.label = label; + this.type = type; } } } diff --git a/app/src/main/res/layout/rv_chat_item.xml b/app/src/main/res/layout/rv_chat_item.xml index 85afe4019..3cd66d3d4 100644 --- a/app/src/main/res/layout/rv_chat_item.xml +++ b/app/src/main/res/layout/rv_chat_item.xml @@ -64,14 +64,37 @@ android:layout_below="@id/previewImage" tools:text="Just another chat message"/> + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7493c0310..3a1d3e960 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -346,4 +346,5 @@ Where did they all hide? Reject You were silenced by a moderator + Failed to sent - tap to retry sending.