Offline works amazingly well

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2020-04-19 03:17:37 +02:00
parent 19e432fd95
commit a6be3e098c
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
36 changed files with 489 additions and 140 deletions

View File

@ -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 {

View File

@ -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')"
]
}
}

View File

@ -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;

View File

@ -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

View File

@ -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"));
}
}
}

View File

@ -101,7 +101,7 @@ class ConversationsRepositoryImpl(val conversationsDao: ConversationsDao) :
userId: Long,
conversations: List<Conversation>,
deleteOutdated: Boolean
): List<Long> {
) {
val map = conversations.map {
it.toConversationEntity()
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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),

View File

@ -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 {

View File

@ -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)
}

View File

@ -35,7 +35,7 @@ interface ConversationsRepository {
userId: Long,
conversations: List<Conversation>,
deleteOutdated: Boolean
): List<Long>
)
suspend fun setChangingValueForConversation(
userId: Long,

View File

@ -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)
}

View File

@ -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

View File

@ -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])
}
}

View File

@ -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.*

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
})
}

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
}
})

View File

@ -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)
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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>