mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-19 19:49:33 +01:00
Offline works amazingly well
Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
parent
19e432fd95
commit
a6be3e098c
@ -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 {
|
||||
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
@ -63,12 +63,21 @@ public class MentionAutocompleteCallback implements AutocompleteCallback<Mention
|
||||
}
|
||||
|
||||
editable.replace(start, end, replacementStringBuilder.toString() + " ");
|
||||
|
||||
String type = "user";
|
||||
|
||||
if (item.source.equals("users")) {
|
||||
// do nothing
|
||||
} else if (item.source.equals("guests")) {
|
||||
type = "guests";
|
||||
} else if (item.source.equals("calls")) {
|
||||
type = "call";
|
||||
}
|
||||
|
||||
Spans.MentionChipSpan mentionChipSpan =
|
||||
new Spans.MentionChipSpan(DisplayUtils.INSTANCE.getDrawableForMentionChipSpan(context,
|
||||
item.id, item.label, conversationUser, item.source,
|
||||
R.xml.chip_you, editText),
|
||||
BetterImageSpan.ALIGN_CENTER,
|
||||
item.id, item.label);
|
||||
R.xml.chip_you, editText), BetterImageSpan.ALIGN_CENTER, item.id, item.label, type);
|
||||
editable.setSpan(mentionChipSpan, start, start + replacementStringBuilder.toString().length(),
|
||||
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
return true;
|
||||
|
@ -28,6 +28,7 @@ import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter
|
||||
import com.nextcloud.talk.newarch.local.models.UserNgEntity
|
||||
import com.nextcloud.talk.newarch.local.models.other.ChatMessageStatus
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.TextMatchers
|
||||
import com.stfalcon.chatkit.commons.models.IMessage
|
||||
@ -81,6 +82,10 @@ class ChatMessage : IMessage, MessageContentType, MessageContentType.Image {
|
||||
@Ignore
|
||||
var jsonMessageId: Long? = null
|
||||
|
||||
@JvmField
|
||||
@JsonField(name = ["referenceId"])
|
||||
var referenceId: String? = null
|
||||
|
||||
@JvmField
|
||||
@JsonField(name = ["token"])
|
||||
var token: String? = null
|
||||
@ -132,6 +137,9 @@ class ChatMessage : IMessage, MessageContentType, MessageContentType.Image {
|
||||
MessageType.SYSTEM_MESSAGE, MessageType.SINGLE_LINK_VIDEO_MESSAGE,
|
||||
MessageType.SINGLE_LINK_AUDIO_MESSAGE, MessageType.SINGLE_LINK_MESSAGE)
|
||||
|
||||
@JsonIgnore
|
||||
var chatMessageStatus: ChatMessageStatus = ChatMessageStatus.RECEIVED
|
||||
|
||||
override fun equals(o: Any?): Boolean {
|
||||
if (this === o) return true
|
||||
if (o !is ChatMessage) return false
|
||||
@ -289,7 +297,7 @@ class ChatMessage : IMessage, MessageContentType, MessageContentType.Image {
|
||||
ApiUtils.getUrlForAvatarWithName(activeUser!!.baseUrl, actorId,
|
||||
R.dimen.avatar_size)
|
||||
}
|
||||
actorType.equals("guests") -> {
|
||||
actorType.equals("guests") || actorType.equals("bots") -> {
|
||||
var apiId: String? = sharedApplication!!.getString(R.string.nc_guest)
|
||||
if (!TextUtils.isEmpty(actorDisplayName)) {
|
||||
apiId = actorDisplayName
|
||||
|
@ -31,10 +31,9 @@ public class ChatUtils {
|
||||
HashMap<String, String> 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ class ConversationsRepositoryImpl(val conversationsDao: ConversationsDao) :
|
||||
userId: Long,
|
||||
conversations: List<Conversation>,
|
||||
deleteOutdated: Boolean
|
||||
): List<Long> {
|
||||
) {
|
||||
val map = conversations.map {
|
||||
it.toConversationEntity()
|
||||
}
|
||||
|
@ -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<List<ChatMessage>> {
|
||||
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<List<ChatMessage>> {
|
||||
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<ChatMessage>): List<Long> {
|
||||
override suspend fun saveMessagesForConversation(user: User, messages: List<ChatMessage>, 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)
|
||||
}
|
||||
}
|
@ -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<ChatOverall> {
|
||||
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<ChatOverall> {
|
||||
val mutableMap = mutableMapOf<String, Int>()
|
||||
mutableMap["lookIntoFuture"] = lookIntoFuture
|
||||
|
@ -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<ChatOverall>
|
||||
|
||||
/*
|
||||
QueryMap items are as follows:
|
||||
- "lookIntoFuture": int (0 or 1),
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ interface ConversationsRepository {
|
||||
userId: Long,
|
||||
conversations: List<Conversation>,
|
||||
deleteOutdated: Boolean
|
||||
): List<Long>
|
||||
)
|
||||
|
||||
suspend fun setChangingValueForConversation(
|
||||
userId: Long,
|
||||
|
@ -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<List<ChatMessage>>
|
||||
fun getPendingMessagesForConversation(conversationId: String): LiveData<List<ChatMessage>>
|
||||
suspend fun getMessageForConversation(conversationId: String, messageId: Long): ChatMessage?
|
||||
fun getMessagesWithUserForConversationSince(conversationId: String, messageId: Long): LiveData<List<ChatMessage>>
|
||||
suspend fun saveMessagesForConversation(messages: List<ChatMessage>): List<Long>
|
||||
suspend fun saveMessagesForConversation(user: User, messages: List<ChatMessage>, sendingMessages: Boolean)
|
||||
suspend fun updateMessageStatus(status: Int, conversationId: String, messageId: Long)
|
||||
}
|
@ -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<ChatOverall>
|
||||
suspend fun getChatMessagesForConversation(user: User, conversationToken: String, lookIntoFuture: Int, lastKnownMessageId: Int, includeLastKnown: Int = 0): Response<ChatOverall>
|
||||
suspend fun getNotificationForUser(user: UserNgEntity, notificationId: String): NotificationOverall
|
||||
suspend fun getParticipantsForCall(user: UserNgEntity, conversationToken: String): ParticipantsOverall
|
||||
|
@ -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<Response<ChatOverall>, Any?>(apiErrorHandler) {
|
||||
override suspend fun run(params: Any?): Response<ChatOverall> {
|
||||
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])
|
||||
}
|
||||
}
|
@ -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.*
|
||||
|
@ -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
|
||||
|
@ -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<T : Any>(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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ class ChatViewLiveDataSource<T : ChatElement>(private val data: LiveData<List<T>
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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<Long> = 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<String, HashMap<String, String>>()
|
||||
val mentionSpans = editable.getSpans(
|
||||
0, editable.length,
|
||||
Spans.MentionChipSpan::class.java
|
||||
)
|
||||
var mentionSpan: Spans.MentionChipSpan
|
||||
val ids = mutableListOf<String>()
|
||||
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<Response<ChatOverall>> {
|
||||
override suspend fun onSuccess(result: Response<ChatOverall>) {
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
*
|
||||
* * Nextcloud Talk application
|
||||
* *
|
||||
* * @author Mario Danic
|
||||
* * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
|
||||
* *
|
||||
* * 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Long>
|
||||
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<ConversationEntity>,
|
||||
deleteOutdated: Boolean
|
||||
): List<Long> {
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<List<MessageEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun saveMessages(vararg messages: MessageEntity): List<Long>
|
||||
|
||||
@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<MessageEntity>
|
||||
|
||||
@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<List<MessageEntity>>
|
||||
|
||||
@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<List<MessageEntity>>
|
||||
|
||||
@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<MessageEntity>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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<String, HashMap<String, String>>? = 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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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<UserNgEntity?> = usersRepository.getActiveUserLiveData()
|
||||
private var currentConversation: Conversation? = null
|
||||
private var currentConversation: MutableLiveData<Conversation?> = MutableLiveData<Conversation?>(null)
|
||||
private val pendingMessages: LiveData<List<ChatMessage>> = Transformations.switchMap(currentConversation) { conversation ->
|
||||
conversation?.let {
|
||||
messagesRepository.getPendingMessagesForConversation(it.databaseId!!)
|
||||
}
|
||||
}
|
||||
|
||||
private var messagesOperations: ConcurrentHashMap<String, Pair<ChatMessage, Int>> = ConcurrentHashMap<String, Pair<ChatMessage, Int>>()
|
||||
|
||||
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<Response<ChatOverall>> {
|
||||
override suspend fun onSuccess(result: Response<ChatOverall>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,14 +64,37 @@
|
||||
android:layout_below="@id/previewImage"
|
||||
tools:text="Just another chat message"/>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:id="@+id/sendingProgressBar"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:visibility="gone"
|
||||
android:progressBackgroundTint="@color/colorPrimary"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_below="@id/chatMessage"
|
||||
android:textSize="10sp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignWithParentIfMissing="true"
|
||||
android:layout_toStartOf="@id/sendingProgressBar"
|
||||
android:id="@+id/messageTime"
|
||||
android:layout_marginEnd="8dp"
|
||||
tools:text="12:30"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:text="@string/nc_failed_to_send"
|
||||
android:layout_below="@id/messageTime"
|
||||
android:id="@+id/failedToSendNotice"
|
||||
android:visibility="gone"
|
||||
android:layout_alignParentEnd="true"/>
|
||||
|
||||
|
||||
</RelativeLayout>
|
@ -346,4 +346,5 @@
|
||||
<string name="nc_search_empty_contacts">Where did they all hide?</string>
|
||||
<string name="nc_reject_call">Reject</string>
|
||||
<string name="silenced_by_moderator">You were silenced by a moderator</string>
|
||||
<string name="nc_failed_to_send">Failed to sent - tap to retry sending.</string>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user