mirror of
https://github.com/nextcloud/talk-android.git
synced 2024-12-18 06:32:08 +03:00
Merge pull request #4425 from nextcloud/backport/4251/stable-20.0
[stable-20.0] Showing temporary messages when queued
This commit is contained in:
commit
d556b9fc89
14 changed files with 490 additions and 45 deletions
|
@ -3,15 +3,19 @@
|
|||
<option name="myName" value="ktlint" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
|
|
|
@ -95,11 +95,14 @@ import com.nextcloud.talk.ui.dialog.AudioOutputDialog
|
|||
import com.nextcloud.talk.ui.dialog.MoreCallActionsDialog
|
||||
import com.nextcloud.talk.users.UserManager
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.SpreedFeatures
|
||||
import com.nextcloud.talk.utils.CapabilitiesUtil
|
||||
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
|
||||
import com.nextcloud.talk.utils.CapabilitiesUtil.isCallRecordingAvailable
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.NotificationUtils.cancelExistingNotificationsForRoom
|
||||
import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
|
||||
import com.nextcloud.talk.utils.ReceiverFlag
|
||||
import com.nextcloud.talk.utils.SpreedFeatures
|
||||
import com.nextcloud.talk.utils.VibrationUtils.vibrateShort
|
||||
import com.nextcloud.talk.utils.animations.PulseAnimation
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY
|
||||
|
@ -117,9 +120,6 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID
|
|||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM
|
||||
import com.nextcloud.talk.utils.CapabilitiesUtil
|
||||
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
|
||||
import com.nextcloud.talk.utils.CapabilitiesUtil.isCallRecordingAvailable
|
||||
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
|
||||
import com.nextcloud.talk.utils.power.PowerManagerUtils
|
||||
import com.nextcloud.talk.utils.registerPermissionHandlerBroadcastReceiver
|
||||
|
@ -129,9 +129,9 @@ import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingConfirmStop
|
|||
import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingErrorState
|
||||
import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingStartedState
|
||||
import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingStartingState
|
||||
import com.nextcloud.talk.webrtc.WebRTCUtils
|
||||
import com.nextcloud.talk.webrtc.PeerConnectionWrapper
|
||||
import com.nextcloud.talk.webrtc.PeerConnectionWrapper.PeerConnectionObserver
|
||||
import com.nextcloud.talk.webrtc.WebRTCUtils
|
||||
import com.nextcloud.talk.webrtc.WebRtcAudioManager
|
||||
import com.nextcloud.talk.webrtc.WebRtcAudioManager.AudioDevice
|
||||
import com.nextcloud.talk.webrtc.WebSocketConnectionHelper
|
||||
|
|
|
@ -65,6 +65,8 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda
|
|||
((SystemMessageViewHolder) holder).assignSystemMessageInterface(chatActivity);
|
||||
} else if (holder instanceof CallStartedViewHolder) {
|
||||
((CallStartedViewHolder) holder).assignCallStartedMessageInterface(chatActivity);
|
||||
} else if (holder instanceof TemporaryMessageViewHolder) {
|
||||
((TemporaryMessageViewHolder) holder).assignTemporaryMessageInterface(chatActivity);
|
||||
}
|
||||
|
||||
super.onBindViewHolder(holder, position);
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
interface TemporaryMessageInterface {
|
||||
fun editTemporaryMessage(id: Int, newMessage: String)
|
||||
fun deleteTemporaryMessage(id: Int)
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* Nextcloud Talk - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.adapters.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import autodagger.AutoInjector
|
||||
import coil.load
|
||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||
import com.nextcloud.talk.databinding.ItemTemporaryMessageBinding
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.message.MessageUtils
|
||||
import com.stfalcon.chatkit.messages.MessagesListAdapter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class TemporaryMessageViewHolder(outgoingView: View, payload: Any) :
|
||||
MessagesListAdapter.OutcomingMessageViewHolder<ChatMessage>(outgoingView) {
|
||||
|
||||
private val binding: ItemTemporaryMessageBinding = ItemTemporaryMessageBinding.bind(outgoingView)
|
||||
|
||||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
@Inject
|
||||
lateinit var context: Context
|
||||
|
||||
@Inject
|
||||
lateinit var messageUtils: MessageUtils
|
||||
|
||||
lateinit var temporaryMessageInterface: TemporaryMessageInterface
|
||||
var isEditing = false
|
||||
|
||||
override fun onBind(message: ChatMessage) {
|
||||
super.onBind(message)
|
||||
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
viewThemeUtils.platform.colorImageView(binding.tempMsgEdit, ColorRole.PRIMARY)
|
||||
viewThemeUtils.platform.colorImageView(binding.tempMsgDelete, ColorRole.PRIMARY)
|
||||
|
||||
binding.tempMsgEdit.setOnClickListener {
|
||||
isEditing = !isEditing
|
||||
if (isEditing) {
|
||||
binding.tempMsgEdit.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
context.resources,
|
||||
R.drawable.ic_check,
|
||||
null
|
||||
)
|
||||
)
|
||||
binding.messageEdit.visibility = View.VISIBLE
|
||||
binding.messageEdit.requestFocus()
|
||||
ViewCompat.getWindowInsetsController(binding.root)?.show(WindowInsetsCompat.Type.ime())
|
||||
binding.messageEdit.setText(binding.messageText.text)
|
||||
binding.messageText.visibility = View.GONE
|
||||
} else {
|
||||
binding.tempMsgEdit.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
context.resources,
|
||||
R.drawable.ic_edit,
|
||||
null
|
||||
)
|
||||
)
|
||||
binding.messageEdit.visibility = View.GONE
|
||||
binding.messageText.visibility = View.VISIBLE
|
||||
val newMessage = binding.messageEdit.text.toString()
|
||||
message.message = newMessage
|
||||
temporaryMessageInterface.editTemporaryMessage(message.tempMessageId, newMessage)
|
||||
}
|
||||
}
|
||||
|
||||
binding.tempMsgDelete.setOnClickListener {
|
||||
temporaryMessageInterface.deleteTemporaryMessage(message.tempMessageId)
|
||||
}
|
||||
|
||||
// parent message handling
|
||||
if (message.parentMessageId != null && message.parentMessageId!! > 0) {
|
||||
processParentMessage(message)
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
|
||||
val bgBubbleColor = bubble.resources.getColor(R.color.bg_message_list_incoming_bubble, null)
|
||||
val layout = R.drawable.shape_outcoming_message
|
||||
val bubbleDrawable = DisplayUtils.getMessageSelector(
|
||||
bgBubbleColor,
|
||||
ResourcesCompat.getColor(bubble.resources, R.color.transparent, null),
|
||||
bgBubbleColor,
|
||||
layout
|
||||
)
|
||||
ViewCompat.setBackground(bubble, bubbleDrawable)
|
||||
}
|
||||
|
||||
private fun processParentMessage(message: ChatMessage) {
|
||||
if (message.parentMessageId != null && !message.isDeleted) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val chatActivity = temporaryMessageInterface as ChatActivity
|
||||
val urlForChatting = ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
chatActivity.conversationUser?.baseUrl,
|
||||
chatActivity.roomToken
|
||||
)
|
||||
|
||||
val parentChatMessage = withContext(Dispatchers.IO) {
|
||||
chatActivity.chatViewModel.getMessageById(
|
||||
urlForChatting,
|
||||
chatActivity.currentConversation!!,
|
||||
message.parentMessageId!!
|
||||
).first()
|
||||
}
|
||||
|
||||
parentChatMessage!!.activeUser = message.activeUser
|
||||
parentChatMessage.imageUrl?.let {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||
val placeholder = context.resources.getDrawable(R.drawable.ic_mimetype_image)
|
||||
binding.messageQuote.quotedMessageImage.setImageDrawable(placeholder)
|
||||
binding.messageQuote.quotedMessageImage.load(it) {
|
||||
addHeader(
|
||||
"Authorization",
|
||||
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||
?: context.getText(R.string.nc_nick_guest)
|
||||
binding.messageQuote.quotedMessage.text = messageUtils
|
||||
.enrichChatReplyMessageText(
|
||||
binding.messageQuote.quotedMessage.context,
|
||||
parentChatMessage,
|
||||
false,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
|
||||
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
|
||||
|
||||
binding.messageQuote.quotedChatMessageView.setOnClickListener {
|
||||
val chatActivity = temporaryMessageInterface as ChatActivity
|
||||
chatActivity.jumpToQuotedMessage(parentChatMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error when processing parent message in view holder", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun assignTemporaryMessageInterface(temporaryMessageInterface: TemporaryMessageInterface) {
|
||||
this.temporaryMessageInterface = temporaryMessageInterface
|
||||
}
|
||||
|
||||
override fun viewDetached() {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun viewAttached() {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
override fun viewRecycled() {
|
||||
// unused atm
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = TemporaryMessageViewHolder::class.java.simpleName
|
||||
}
|
||||
}
|
|
@ -101,6 +101,8 @@ import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder
|
|||
import com.nextcloud.talk.adapters.messages.SystemMessageInterface
|
||||
import com.nextcloud.talk.adapters.messages.SystemMessageViewHolder
|
||||
import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
|
||||
import com.nextcloud.talk.adapters.messages.TemporaryMessageInterface
|
||||
import com.nextcloud.talk.adapters.messages.TemporaryMessageViewHolder
|
||||
import com.nextcloud.talk.adapters.messages.UnreadNoticeMessageViewHolder
|
||||
import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
|
@ -213,7 +215,8 @@ class ChatActivity :
|
|||
CommonMessageInterface,
|
||||
PreviewMessageInterface,
|
||||
SystemMessageInterface,
|
||||
CallStartedMessageInterface {
|
||||
CallStartedMessageInterface,
|
||||
TemporaryMessageInterface {
|
||||
|
||||
var active = false
|
||||
|
||||
|
@ -534,6 +537,37 @@ class ChatActivity :
|
|||
private fun initObservers() {
|
||||
Log.d(TAG, "initObservers Called")
|
||||
|
||||
messageInputViewModel.messageQueueFlow.observe(this) { list ->
|
||||
list.forEachIndexed { _, qMsg ->
|
||||
Log.d("Julius", "Message recieved: ${qMsg.message}")
|
||||
val temporaryChatMessage = ChatMessage()
|
||||
temporaryChatMessage.jsonMessageId = -3
|
||||
temporaryChatMessage.actorId = "-3"
|
||||
temporaryChatMessage.timestamp = System.currentTimeMillis() / 1000
|
||||
temporaryChatMessage.message = qMsg.message.toString()
|
||||
temporaryChatMessage.tempMessageId = qMsg.id
|
||||
temporaryChatMessage.isTempMessage = true
|
||||
temporaryChatMessage.parentMessageId = qMsg.replyTo!!.toLong()
|
||||
val pos = adapter?.getMessagePositionById(qMsg.replyTo.toString())
|
||||
adapter?.addToStart(temporaryChatMessage, true)
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
messageInputViewModel.messageQueueSizeFlow.observe(this) { size ->
|
||||
if (size == 0) {
|
||||
var i = 0
|
||||
var pos = adapter?.getMessagePositionById("-3")
|
||||
while (pos != null && pos > -1) {
|
||||
adapter?.items?.removeAt(pos)
|
||||
i++
|
||||
pos = adapter?.getMessagePositionById("-3")
|
||||
}
|
||||
adapter?.notifyDataSetChanged()
|
||||
Log.d("Julius", "End i: $i")
|
||||
}
|
||||
}
|
||||
|
||||
this.lifecycleScope.launch {
|
||||
chatViewModel.getConversationFlow
|
||||
.onEach { conversationModel ->
|
||||
|
@ -620,6 +654,7 @@ class ChatActivity :
|
|||
withCredentials = credentials!!,
|
||||
withUrl = urlForChatting
|
||||
)
|
||||
messageInputViewModel.getTempMessagesFromMessageQueue(currentConversation!!.internalId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1170,6 +1205,17 @@ class ChatActivity :
|
|||
this
|
||||
)
|
||||
|
||||
messageHolders.registerContentType(
|
||||
CONTENT_TYPE_TEMP,
|
||||
TemporaryMessageViewHolder::class.java,
|
||||
payload,
|
||||
R.layout.item_temporary_message,
|
||||
TemporaryMessageViewHolder::class.java,
|
||||
payload,
|
||||
R.layout.item_temporary_message,
|
||||
this
|
||||
)
|
||||
|
||||
messageHolders.registerContentType(
|
||||
CONTENT_TYPE_SYSTEM_MESSAGE,
|
||||
SystemMessageViewHolder::class.java,
|
||||
|
@ -2330,8 +2376,8 @@ class ChatActivity :
|
|||
try {
|
||||
EmojiCompat.get().process(currentConversation?.displayName as CharSequence).toString()
|
||||
} catch (e: java.lang.IllegalStateException) {
|
||||
Log.e(TAG, "setActionBarTitle failed $e")
|
||||
currentConversation?.displayName
|
||||
error(e)
|
||||
}
|
||||
} else {
|
||||
""
|
||||
|
@ -2445,9 +2491,9 @@ class ChatActivity :
|
|||
|
||||
if (currentConversation!!.remoteServer != null) {
|
||||
val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V3, 2, 1))
|
||||
ncApi!!.getSignalingSettings(
|
||||
ncApi.getSignalingSettings(
|
||||
credentials,
|
||||
ApiUtils.getUrlForSignalingSettings(apiVersion, conversationUser!!.baseUrl, roomToken!!)
|
||||
ApiUtils.getUrlForSignalingSettings(apiVersion, conversationUser!!.baseUrl, roomToken)
|
||||
).blockingSubscribe(object : Observer<SignalingSettingsOverall> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
// unused atm
|
||||
|
@ -3072,7 +3118,10 @@ class ChatActivity :
|
|||
|
||||
private fun openMessageActionsDialog(iMessage: IMessage?) {
|
||||
val message = iMessage as ChatMessage
|
||||
if (hasVisibleItems(message) && !isSystemMessage(message)) {
|
||||
if (hasVisibleItems(message) &&
|
||||
!isSystemMessage(message) &&
|
||||
message.id != "-3"
|
||||
) {
|
||||
MessageActionsDialog(
|
||||
this,
|
||||
message,
|
||||
|
@ -3475,6 +3524,7 @@ class ChatActivity :
|
|||
CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
|
||||
CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
|
||||
CONTENT_TYPE_CALL_STARTED -> message.id == "-2"
|
||||
CONTENT_TYPE_TEMP -> message.id == "-3"
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
@ -3626,6 +3676,30 @@ class ChatActivity :
|
|||
startACall(false, false)
|
||||
}
|
||||
|
||||
override fun editTemporaryMessage(id: Int, newMessage: String) {
|
||||
messageInputViewModel.editQueuedMessage(currentConversation!!.internalId, id, newMessage)
|
||||
adapter?.notifyDataSetChanged() // TODO optimize this
|
||||
}
|
||||
|
||||
override fun deleteTemporaryMessage(id: Int) {
|
||||
messageInputViewModel.removeFromQueue(currentConversation!!.internalId, id)
|
||||
var i = 0
|
||||
val max = messageInputViewModel.messageQueueSizeFlow.value?.plus(1)
|
||||
for (item in adapter?.items!!) {
|
||||
if (i > max!! && max < 1) break
|
||||
if (item.item is ChatMessage &&
|
||||
(item.item as ChatMessage).isTempMessage &&
|
||||
(item.item as ChatMessage).tempMessageId == id
|
||||
) {
|
||||
val index = adapter?.items!!.indexOf(item)
|
||||
adapter?.items!!.removeAt(index)
|
||||
adapter?.notifyItemRemoved(index)
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
private fun logConversationInfos(methodName: String) {
|
||||
Log.d(TAG, " |-----------------------------------------------")
|
||||
Log.d(TAG, " | method: $methodName")
|
||||
|
@ -3655,6 +3729,7 @@ class ChatActivity :
|
|||
private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 5
|
||||
private const val CONTENT_TYPE_POLL: Byte = 6
|
||||
private const val CONTENT_TYPE_LINK_PREVIEW: Byte = 7
|
||||
private const val CONTENT_TYPE_TEMP: Byte = 8
|
||||
private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200
|
||||
private const val GET_ROOM_INFO_DELAY_NORMAL: Long = 30000
|
||||
private const val GET_ROOM_INFO_DELAY_LOBBY: Long = 5000
|
||||
|
|
|
@ -73,7 +73,6 @@ import com.nextcloud.talk.utils.text.Spans
|
|||
import com.otaliastudios.autocomplete.Autocomplete
|
||||
import com.stfalcon.chatkit.commons.models.IMessage
|
||||
import com.vanniktech.emoji.EmojiPopup
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -144,7 +143,7 @@ class MessageInputFragment : Fragment() {
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
chatActivity.messageInputViewModel.restoreMessageQueue(chatActivity.roomToken)
|
||||
chatActivity.messageInputViewModel.restoreMessageQueue(chatActivity.currentConversation!!.internalId)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
@ -179,18 +178,20 @@ class MessageInputFragment : Fragment() {
|
|||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
var wasOnline = true
|
||||
networkMonitor.isOnline.onEach { isOnline ->
|
||||
val connectionGained = (!wasOnline && isOnline)
|
||||
wasOnline = !binding.fragmentMessageInputView.isShown
|
||||
Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained")
|
||||
delay(500)
|
||||
handleMessageQueue(isOnline)
|
||||
handleUI(isOnline, connectionGained)
|
||||
}.collect()
|
||||
var wasOnline: Boolean
|
||||
networkMonitor.isOnline
|
||||
.onEach { isOnline ->
|
||||
wasOnline = !binding.fragmentConnectionLost.isShown
|
||||
val connectionGained = (!wasOnline && isOnline)
|
||||
Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained")
|
||||
handleMessageQueue(isOnline)
|
||||
handleUI(isOnline, connectionGained)
|
||||
}.collect()
|
||||
}
|
||||
|
||||
chatActivity.messageInputViewModel.messageQueueSizeFlow.observe(viewLifecycleOwner) { size ->
|
||||
Log.d("Julius", "MessageQueueSizeFlow recieved: $size")
|
||||
|
||||
if (size > 0) {
|
||||
binding.fragmentConnectionLost.text = getString(R.string.connection_lost_queued, size)
|
||||
} else {
|
||||
|
@ -233,7 +234,7 @@ class MessageInputFragment : Fragment() {
|
|||
binding.fragmentConnectionLost.clearAnimation()
|
||||
binding.fragmentConnectionLost.visibility = View.GONE
|
||||
binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityRed))
|
||||
binding.fragmentConnectionLost.text = getString(R.string.connection_lost_sent_messages_are_queued)
|
||||
// binding.fragmentConnectionLost.text = getString(R.string.connection_lost_sent_messages_are_queued)
|
||||
binding.fragmentConnectionLost.visibility = View.VISIBLE
|
||||
binding.fragmentMessageInputView.attachmentButton.isEnabled = false
|
||||
binding.fragmentMessageInputView.recordAudioButton.isEnabled = false
|
||||
|
@ -244,7 +245,7 @@ class MessageInputFragment : Fragment() {
|
|||
if (isOnline) {
|
||||
chatActivity.messageInputViewModel.switchToMessageQueue(false)
|
||||
chatActivity.messageInputViewModel.sendAndEmptyMessageQueue(
|
||||
chatActivity.roomToken,
|
||||
chatActivity.currentConversation!!.internalId,
|
||||
chatActivity.conversationUser!!.getCredentials(),
|
||||
ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
|
@ -793,7 +794,7 @@ class MessageInputFragment : Fragment() {
|
|||
|
||||
private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) {
|
||||
chatActivity.messageInputViewModel.sendChatMessage(
|
||||
chatActivity.roomToken,
|
||||
chatActivity.currentConversation!!.internalId,
|
||||
chatActivity.conversationUser!!.getCredentials(),
|
||||
ApiUtils.getUrlForChat(
|
||||
chatActivity.chatApiVersion,
|
||||
|
|
|
@ -111,7 +111,11 @@ data class ChatMessage(
|
|||
|
||||
var hiddenByCollapse: Boolean = false,
|
||||
|
||||
var openWhenDownloaded: Boolean = true
|
||||
var openWhenDownloaded: Boolean = true,
|
||||
|
||||
var isTempMessage: Boolean = false,
|
||||
|
||||
var tempMessageId: Int = -1
|
||||
|
||||
) : MessageContentType, MessageContentType.Image {
|
||||
|
||||
|
|
|
@ -48,7 +48,8 @@ class MessageInputViewModel @Inject constructor(
|
|||
val disposableSet = mutableSetOf<Disposable>()
|
||||
|
||||
data class QueuedMessage(
|
||||
val message: CharSequence? = null,
|
||||
val id: Int,
|
||||
var message: CharSequence? = null,
|
||||
val displayName: String? = null,
|
||||
val replyTo: Int? = null,
|
||||
val sendWithoutNotification: Boolean? = null
|
||||
|
@ -124,9 +125,13 @@ class MessageInputViewModel @Inject constructor(
|
|||
val messageQueueSizeFlow: LiveData<Int>
|
||||
get() = _messageQueueSizeFlow.asLiveData()
|
||||
|
||||
private val _messageQueueFlow: MutableLiveData<List<QueuedMessage>> = MutableLiveData()
|
||||
val messageQueueFlow: LiveData<List<QueuedMessage>>
|
||||
get() = _messageQueueFlow
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun sendChatMessage(
|
||||
roomToken: String,
|
||||
internalId: String,
|
||||
credentials: String,
|
||||
url: String,
|
||||
message: CharSequence,
|
||||
|
@ -135,9 +140,13 @@ class MessageInputViewModel @Inject constructor(
|
|||
sendWithoutNotification: Boolean
|
||||
) {
|
||||
if (isQueueing) {
|
||||
messageQueue.add(QueuedMessage(message, displayName, replyTo, sendWithoutNotification))
|
||||
dataStore.saveMessageQueue(roomToken, messageQueue)
|
||||
val tempID = System.currentTimeMillis().toInt()
|
||||
val qMsg = QueuedMessage(tempID, message, displayName, replyTo, sendWithoutNotification)
|
||||
messageQueue = dataStore.getMessageQueue(internalId)
|
||||
messageQueue.add(qMsg)
|
||||
dataStore.saveMessageQueue(internalId, messageQueue)
|
||||
_messageQueueSizeFlow.update { messageQueue.size }
|
||||
_messageQueueFlow.postValue(listOf(qMsg))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -242,17 +251,16 @@ class MessageInputViewModel @Inject constructor(
|
|||
_getRecordingTime.postValue(time)
|
||||
}
|
||||
|
||||
fun sendAndEmptyMessageQueue(roomToken: String, credentials: String, url: String) {
|
||||
fun sendAndEmptyMessageQueue(internalId: String, credentials: String, url: String) {
|
||||
if (isQueueing) return
|
||||
messageQueue.clear()
|
||||
|
||||
val queue = dataStore.getMessageQueue(roomToken)
|
||||
dataStore.saveMessageQueue(roomToken, null) // empties the queue
|
||||
val queue = dataStore.getMessageQueue(internalId)
|
||||
dataStore.saveMessageQueue(internalId, null) // empties the queue
|
||||
while (queue.size > 0) {
|
||||
val msg = queue.removeFirst()
|
||||
sleep(DELAY_BETWEEN_QUEUED_MESSAGES)
|
||||
val msg = queue.removeAt(0)
|
||||
sendChatMessage(
|
||||
roomToken,
|
||||
internalId,
|
||||
credentials,
|
||||
url,
|
||||
msg.message!!,
|
||||
|
@ -260,20 +268,55 @@ class MessageInputViewModel @Inject constructor(
|
|||
msg.replyTo!!,
|
||||
msg.sendWithoutNotification!!
|
||||
)
|
||||
sleep(DELAY_BETWEEN_QUEUED_MESSAGES)
|
||||
}
|
||||
_messageQueueSizeFlow.tryEmit(0)
|
||||
}
|
||||
|
||||
fun getTempMessagesFromMessageQueue(internalId: String) {
|
||||
val queue = dataStore.getMessageQueue(internalId)
|
||||
val list = mutableListOf<QueuedMessage>()
|
||||
for (msg in queue) {
|
||||
Log.d("Julius", "Msg: ${msg.message}")
|
||||
list.add(msg)
|
||||
}
|
||||
_messageQueueFlow.postValue(list)
|
||||
}
|
||||
|
||||
fun switchToMessageQueue(shouldQueue: Boolean) {
|
||||
isQueueing = shouldQueue
|
||||
}
|
||||
|
||||
fun restoreMessageQueue(roomToken: String) {
|
||||
messageQueue = dataStore.getMessageQueue(roomToken)
|
||||
fun restoreMessageQueue(internalId: String) {
|
||||
messageQueue = dataStore.getMessageQueue(internalId)
|
||||
_messageQueueSizeFlow.tryEmit(messageQueue.size)
|
||||
}
|
||||
|
||||
fun removeFromQueue(internalId: String, id: Int) {
|
||||
val queue = dataStore.getMessageQueue(internalId)
|
||||
for (qMsg in queue) {
|
||||
if (qMsg.id == id) {
|
||||
queue.remove(qMsg)
|
||||
break
|
||||
}
|
||||
}
|
||||
dataStore.saveMessageQueue(internalId, queue)
|
||||
_messageQueueSizeFlow.tryEmit(queue.size)
|
||||
}
|
||||
|
||||
fun editQueuedMessage(internalId: String, id: Int, newMessage: String) {
|
||||
val queue = dataStore.getMessageQueue(internalId)
|
||||
for (qMsg in queue) {
|
||||
if (qMsg.id == id) {
|
||||
qMsg.message = newMessage
|
||||
break
|
||||
}
|
||||
}
|
||||
dataStore.saveMessageQueue(internalId, queue)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = MessageInputViewModel::class.java.simpleName
|
||||
private const val DELAY_BETWEEN_QUEUED_MESSAGES: Long = 100
|
||||
private const val DELAY_BETWEEN_QUEUED_MESSAGES: Long = 1000
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import kotlinx.coroutines.channels.awaitClose
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
@ -73,6 +74,7 @@ class NetworkMonitorImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.conflate()
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import com.nextcloud.talk.models.json.generic.GenericOverall;
|
|||
import com.nextcloud.talk.models.json.push.PushConfigurationState;
|
||||
import com.nextcloud.talk.users.UserManager;
|
||||
import com.nextcloud.talk.utils.ApiUtils;
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences;
|
||||
import com.nextcloud.talk.webrtc.WebSocketConnectionHelper;
|
||||
|
||||
import java.net.CookieManager;
|
||||
|
@ -53,6 +54,8 @@ public class AccountRemovalWorker extends Worker {
|
|||
|
||||
@Inject ArbitraryStorageManager arbitraryStorageManager;
|
||||
|
||||
@Inject AppPreferences appPreferences;
|
||||
|
||||
@Inject Retrofit retrofit;
|
||||
|
||||
@Inject OkHttpClient okHttpClient;
|
||||
|
@ -193,6 +196,7 @@ public class AccountRemovalWorker extends Worker {
|
|||
if (user.getId() != null) {
|
||||
String username = user.getUsername();
|
||||
try {
|
||||
appPreferences.deleteAllMessageQueuesFor(user.getUserId());
|
||||
userManager.deleteUser(user.getId());
|
||||
Log.d(TAG, "deleted user: " + username);
|
||||
} catch (Throwable e) {
|
||||
|
|
|
@ -176,6 +176,8 @@ public interface AppPreferences {
|
|||
|
||||
List<MessageInputViewModel.QueuedMessage> getMessageQueue(String internalConversationId);
|
||||
|
||||
void deleteAllMessageQueuesFor(String userId);
|
||||
|
||||
|
||||
void clear();
|
||||
}
|
||||
|
|
|
@ -484,7 +484,10 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
|
|||
var queueStr = ""
|
||||
queue?.let {
|
||||
for (msg in queue) {
|
||||
val msgStr = "${msg.message},${msg.replyTo},${msg.displayName},${msg.sendWithoutNotification}^"
|
||||
val msgStr = "${msg.id},${msg.message},${msg.replyTo},${msg.displayName},${
|
||||
msg
|
||||
.sendWithoutNotification
|
||||
}^"
|
||||
queueStr += msgStr
|
||||
}
|
||||
}
|
||||
|
@ -504,12 +507,13 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
|
|||
try {
|
||||
if (msgStr.isNotEmpty()) {
|
||||
val msgArray = msgStr.split(",")
|
||||
val id = msgArray[ID].toInt()
|
||||
val message = msgArray[MESSAGE_INDEX]
|
||||
val replyTo = msgArray[REPLY_TO_INDEX].toInt()
|
||||
val displayName = msgArray[DISPLY_NAME_INDEX]
|
||||
val displayName = msgArray[DISPLAY_NAME_INDEX]
|
||||
val silent = msgArray[SILENT_INDEX].toBoolean()
|
||||
|
||||
val qMsg = MessageInputViewModel.QueuedMessage(message, displayName, replyTo, silent)
|
||||
val qMsg = MessageInputViewModel.QueuedMessage(id, message, displayName, replyTo, silent)
|
||||
queue.add(qMsg)
|
||||
}
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
|
@ -520,6 +524,26 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
|
|||
return queue
|
||||
}
|
||||
|
||||
override fun deleteAllMessageQueuesFor(userId: String) {
|
||||
runBlocking {
|
||||
async {
|
||||
val keyList = mutableListOf<Preferences.Key<*>>()
|
||||
val preferencesMap = context.dataStore.data.first().asMap()
|
||||
for (preference in preferencesMap) {
|
||||
if (preference.key.name.contains("$userId@")) {
|
||||
keyList.add(preference.key)
|
||||
}
|
||||
}
|
||||
|
||||
for (key in keyList) {
|
||||
context.dataStore.edit {
|
||||
it.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun clear() {}
|
||||
|
||||
private suspend fun writeString(key: String, value: String) =
|
||||
|
@ -572,10 +596,11 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
|
|||
@Suppress("UnusedPrivateProperty")
|
||||
private val TAG = AppPreferencesImpl::class.simpleName
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||
private const val MESSAGE_INDEX: Int = 0
|
||||
private const val REPLY_TO_INDEX: Int = 1
|
||||
private const val DISPLY_NAME_INDEX: Int = 2
|
||||
private const val SILENT_INDEX: Int = 3
|
||||
private const val ID: Int = 0
|
||||
private const val MESSAGE_INDEX: Int = 1
|
||||
private const val REPLY_TO_INDEX: Int = 2
|
||||
private const val DISPLAY_NAME_INDEX: Int = 3
|
||||
private const val SILENT_INDEX: Int = 4
|
||||
const val PROXY_TYPE = "proxy_type"
|
||||
const val PROXY_SERVER = "proxy_server"
|
||||
const val PROXY_HOST = "proxy_host"
|
||||
|
|
78
app/src/main/res/layout/item_temporary_message.xml
Normal file
78
app/src/main/res/layout/item_temporary_message.xml
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Nextcloud Talk - Android Client
|
||||
~
|
||||
~ SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
|
||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginBottom="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_centerVertical="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/temp_msg_edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_edit"
|
||||
android:paddingHorizontal="@dimen/standard_half_padding"
|
||||
android:layout_marginEnd="@dimen/standard_quarter_margin" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/temp_msg_delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_delete"
|
||||
android:paddingHorizontal="@dimen/standard_half_padding"
|
||||
android:layout_marginStart="@dimen/standard_quarter_margin" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:id="@id/bubble"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="@dimen/message_outcoming_bubble_margin_left"
|
||||
app:alignContent="stretch"
|
||||
app:alignItems="stretch"
|
||||
app:flexWrap="wrap"
|
||||
app:justifyContent="flex_end">
|
||||
|
||||
<include
|
||||
android:id="@+id/message_quote"
|
||||
layout="@layout/item_message_quote"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<androidx.emoji2.widget.EmojiTextView
|
||||
android:id="@id/messageText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColorHighlight="@color/nc_grey"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Talk to you later!" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/message_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
</RelativeLayout>
|
Loading…
Reference in a new issue