Merge pull request #4425 from nextcloud/backport/4251/stable-20.0

[stable-20.0] Showing temporary messages when queued
This commit is contained in:
Marcel Hibbe 2024-11-08 13:04:33 +01:00 committed by GitHub
commit d556b9fc89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 490 additions and 45 deletions

View file

@ -3,15 +3,19 @@
<option name="myName" value="ktlint" /> <option name="myName" value="ktlint" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />

View file

@ -95,11 +95,14 @@ import com.nextcloud.talk.ui.dialog.AudioOutputDialog
import com.nextcloud.talk.ui.dialog.MoreCallActionsDialog import com.nextcloud.talk.ui.dialog.MoreCallActionsDialog
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils 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.DisplayUtils
import com.nextcloud.talk.utils.NotificationUtils.cancelExistingNotificationsForRoom import com.nextcloud.talk.utils.NotificationUtils.cancelExistingNotificationsForRoom
import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
import com.nextcloud.talk.utils.ReceiverFlag import com.nextcloud.talk.utils.ReceiverFlag
import com.nextcloud.talk.utils.SpreedFeatures
import com.nextcloud.talk.utils.VibrationUtils.vibrateShort import com.nextcloud.talk.utils.VibrationUtils.vibrateShort
import com.nextcloud.talk.utils.animations.PulseAnimation import com.nextcloud.talk.utils.animations.PulseAnimation
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY 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_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH 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.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.permissions.PlatformPermissionUtil
import com.nextcloud.talk.utils.power.PowerManagerUtils import com.nextcloud.talk.utils.power.PowerManagerUtils
import com.nextcloud.talk.utils.registerPermissionHandlerBroadcastReceiver 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.RecordingErrorState
import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingStartedState import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingStartedState
import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingStartingState 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
import com.nextcloud.talk.webrtc.PeerConnectionWrapper.PeerConnectionObserver 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
import com.nextcloud.talk.webrtc.WebRtcAudioManager.AudioDevice import com.nextcloud.talk.webrtc.WebRtcAudioManager.AudioDevice
import com.nextcloud.talk.webrtc.WebSocketConnectionHelper import com.nextcloud.talk.webrtc.WebSocketConnectionHelper

View file

@ -65,6 +65,8 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda
((SystemMessageViewHolder) holder).assignSystemMessageInterface(chatActivity); ((SystemMessageViewHolder) holder).assignSystemMessageInterface(chatActivity);
} else if (holder instanceof CallStartedViewHolder) { } else if (holder instanceof CallStartedViewHolder) {
((CallStartedViewHolder) holder).assignCallStartedMessageInterface(chatActivity); ((CallStartedViewHolder) holder).assignCallStartedMessageInterface(chatActivity);
} else if (holder instanceof TemporaryMessageViewHolder) {
((TemporaryMessageViewHolder) holder).assignTemporaryMessageInterface(chatActivity);
} }
super.onBindViewHolder(holder, position); super.onBindViewHolder(holder, position);

View file

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

View file

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

View file

@ -101,6 +101,8 @@ import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder
import com.nextcloud.talk.adapters.messages.SystemMessageInterface import com.nextcloud.talk.adapters.messages.SystemMessageInterface
import com.nextcloud.talk.adapters.messages.SystemMessageViewHolder import com.nextcloud.talk.adapters.messages.SystemMessageViewHolder
import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter 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.UnreadNoticeMessageViewHolder
import com.nextcloud.talk.adapters.messages.VoiceMessageInterface import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
@ -213,7 +215,8 @@ class ChatActivity :
CommonMessageInterface, CommonMessageInterface,
PreviewMessageInterface, PreviewMessageInterface,
SystemMessageInterface, SystemMessageInterface,
CallStartedMessageInterface { CallStartedMessageInterface,
TemporaryMessageInterface {
var active = false var active = false
@ -534,6 +537,37 @@ class ChatActivity :
private fun initObservers() { private fun initObservers() {
Log.d(TAG, "initObservers Called") 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 { this.lifecycleScope.launch {
chatViewModel.getConversationFlow chatViewModel.getConversationFlow
.onEach { conversationModel -> .onEach { conversationModel ->
@ -620,6 +654,7 @@ class ChatActivity :
withCredentials = credentials!!, withCredentials = credentials!!,
withUrl = urlForChatting withUrl = urlForChatting
) )
messageInputViewModel.getTempMessagesFromMessageQueue(currentConversation!!.internalId)
} }
} }
@ -1170,6 +1205,17 @@ class ChatActivity :
this 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( messageHolders.registerContentType(
CONTENT_TYPE_SYSTEM_MESSAGE, CONTENT_TYPE_SYSTEM_MESSAGE,
SystemMessageViewHolder::class.java, SystemMessageViewHolder::class.java,
@ -2330,8 +2376,8 @@ class ChatActivity :
try { try {
EmojiCompat.get().process(currentConversation?.displayName as CharSequence).toString() EmojiCompat.get().process(currentConversation?.displayName as CharSequence).toString()
} catch (e: java.lang.IllegalStateException) { } catch (e: java.lang.IllegalStateException) {
Log.e(TAG, "setActionBarTitle failed $e")
currentConversation?.displayName currentConversation?.displayName
error(e)
} }
} else { } else {
"" ""
@ -2445,9 +2491,9 @@ class ChatActivity :
if (currentConversation!!.remoteServer != null) { if (currentConversation!!.remoteServer != null) {
val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V3, 2, 1)) val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V3, 2, 1))
ncApi!!.getSignalingSettings( ncApi.getSignalingSettings(
credentials, credentials,
ApiUtils.getUrlForSignalingSettings(apiVersion, conversationUser!!.baseUrl, roomToken!!) ApiUtils.getUrlForSignalingSettings(apiVersion, conversationUser!!.baseUrl, roomToken)
).blockingSubscribe(object : Observer<SignalingSettingsOverall> { ).blockingSubscribe(object : Observer<SignalingSettingsOverall> {
override fun onSubscribe(d: Disposable) { override fun onSubscribe(d: Disposable) {
// unused atm // unused atm
@ -3072,7 +3118,10 @@ class ChatActivity :
private fun openMessageActionsDialog(iMessage: IMessage?) { private fun openMessageActionsDialog(iMessage: IMessage?) {
val message = iMessage as ChatMessage val message = iMessage as ChatMessage
if (hasVisibleItems(message) && !isSystemMessage(message)) { if (hasVisibleItems(message) &&
!isSystemMessage(message) &&
message.id != "-3"
) {
MessageActionsDialog( MessageActionsDialog(
this, this,
message, message,
@ -3475,6 +3524,7 @@ class ChatActivity :
CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage) CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1" CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
CONTENT_TYPE_CALL_STARTED -> message.id == "-2" CONTENT_TYPE_CALL_STARTED -> message.id == "-2"
CONTENT_TYPE_TEMP -> message.id == "-3"
else -> false else -> false
} }
@ -3626,6 +3676,30 @@ class ChatActivity :
startACall(false, false) 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) { private fun logConversationInfos(methodName: String) {
Log.d(TAG, " |-----------------------------------------------") Log.d(TAG, " |-----------------------------------------------")
Log.d(TAG, " | method: $methodName") 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_VOICE_MESSAGE: Byte = 5
private const val CONTENT_TYPE_POLL: Byte = 6 private const val CONTENT_TYPE_POLL: Byte = 6
private const val CONTENT_TYPE_LINK_PREVIEW: Byte = 7 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 NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200
private const val GET_ROOM_INFO_DELAY_NORMAL: Long = 30000 private const val GET_ROOM_INFO_DELAY_NORMAL: Long = 30000
private const val GET_ROOM_INFO_DELAY_LOBBY: Long = 5000 private const val GET_ROOM_INFO_DELAY_LOBBY: Long = 5000

View file

@ -73,7 +73,6 @@ import com.nextcloud.talk.utils.text.Spans
import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.Autocomplete
import com.stfalcon.chatkit.commons.models.IMessage import com.stfalcon.chatkit.commons.models.IMessage
import com.vanniktech.emoji.EmojiPopup import com.vanniktech.emoji.EmojiPopup
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -144,7 +143,7 @@ class MessageInputFragment : Fragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
chatActivity.messageInputViewModel.restoreMessageQueue(chatActivity.roomToken) chatActivity.messageInputViewModel.restoreMessageQueue(chatActivity.currentConversation!!.internalId)
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -179,18 +178,20 @@ class MessageInputFragment : Fragment() {
} }
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
var wasOnline = true var wasOnline: Boolean
networkMonitor.isOnline.onEach { isOnline -> networkMonitor.isOnline
.onEach { isOnline ->
wasOnline = !binding.fragmentConnectionLost.isShown
val connectionGained = (!wasOnline && isOnline) val connectionGained = (!wasOnline && isOnline)
wasOnline = !binding.fragmentMessageInputView.isShown
Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained") Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained")
delay(500)
handleMessageQueue(isOnline) handleMessageQueue(isOnline)
handleUI(isOnline, connectionGained) handleUI(isOnline, connectionGained)
}.collect() }.collect()
} }
chatActivity.messageInputViewModel.messageQueueSizeFlow.observe(viewLifecycleOwner) { size -> chatActivity.messageInputViewModel.messageQueueSizeFlow.observe(viewLifecycleOwner) { size ->
Log.d("Julius", "MessageQueueSizeFlow recieved: $size")
if (size > 0) { if (size > 0) {
binding.fragmentConnectionLost.text = getString(R.string.connection_lost_queued, size) binding.fragmentConnectionLost.text = getString(R.string.connection_lost_queued, size)
} else { } else {
@ -233,7 +234,7 @@ class MessageInputFragment : Fragment() {
binding.fragmentConnectionLost.clearAnimation() binding.fragmentConnectionLost.clearAnimation()
binding.fragmentConnectionLost.visibility = View.GONE binding.fragmentConnectionLost.visibility = View.GONE
binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityRed)) 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.fragmentConnectionLost.visibility = View.VISIBLE
binding.fragmentMessageInputView.attachmentButton.isEnabled = false binding.fragmentMessageInputView.attachmentButton.isEnabled = false
binding.fragmentMessageInputView.recordAudioButton.isEnabled = false binding.fragmentMessageInputView.recordAudioButton.isEnabled = false
@ -244,7 +245,7 @@ class MessageInputFragment : Fragment() {
if (isOnline) { if (isOnline) {
chatActivity.messageInputViewModel.switchToMessageQueue(false) chatActivity.messageInputViewModel.switchToMessageQueue(false)
chatActivity.messageInputViewModel.sendAndEmptyMessageQueue( chatActivity.messageInputViewModel.sendAndEmptyMessageQueue(
chatActivity.roomToken, chatActivity.currentConversation!!.internalId,
chatActivity.conversationUser!!.getCredentials(), chatActivity.conversationUser!!.getCredentials(),
ApiUtils.getUrlForChat( ApiUtils.getUrlForChat(
chatActivity.chatApiVersion, chatActivity.chatApiVersion,
@ -793,7 +794,7 @@ class MessageInputFragment : Fragment() {
private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) { private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) {
chatActivity.messageInputViewModel.sendChatMessage( chatActivity.messageInputViewModel.sendChatMessage(
chatActivity.roomToken, chatActivity.currentConversation!!.internalId,
chatActivity.conversationUser!!.getCredentials(), chatActivity.conversationUser!!.getCredentials(),
ApiUtils.getUrlForChat( ApiUtils.getUrlForChat(
chatActivity.chatApiVersion, chatActivity.chatApiVersion,

View file

@ -111,7 +111,11 @@ data class ChatMessage(
var hiddenByCollapse: Boolean = false, var hiddenByCollapse: Boolean = false,
var openWhenDownloaded: Boolean = true var openWhenDownloaded: Boolean = true,
var isTempMessage: Boolean = false,
var tempMessageId: Int = -1
) : MessageContentType, MessageContentType.Image { ) : MessageContentType, MessageContentType.Image {

View file

@ -48,7 +48,8 @@ class MessageInputViewModel @Inject constructor(
val disposableSet = mutableSetOf<Disposable>() val disposableSet = mutableSetOf<Disposable>()
data class QueuedMessage( data class QueuedMessage(
val message: CharSequence? = null, val id: Int,
var message: CharSequence? = null,
val displayName: String? = null, val displayName: String? = null,
val replyTo: Int? = null, val replyTo: Int? = null,
val sendWithoutNotification: Boolean? = null val sendWithoutNotification: Boolean? = null
@ -124,9 +125,13 @@ class MessageInputViewModel @Inject constructor(
val messageQueueSizeFlow: LiveData<Int> val messageQueueSizeFlow: LiveData<Int>
get() = _messageQueueSizeFlow.asLiveData() get() = _messageQueueSizeFlow.asLiveData()
private val _messageQueueFlow: MutableLiveData<List<QueuedMessage>> = MutableLiveData()
val messageQueueFlow: LiveData<List<QueuedMessage>>
get() = _messageQueueFlow
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun sendChatMessage( fun sendChatMessage(
roomToken: String, internalId: String,
credentials: String, credentials: String,
url: String, url: String,
message: CharSequence, message: CharSequence,
@ -135,9 +140,13 @@ class MessageInputViewModel @Inject constructor(
sendWithoutNotification: Boolean sendWithoutNotification: Boolean
) { ) {
if (isQueueing) { if (isQueueing) {
messageQueue.add(QueuedMessage(message, displayName, replyTo, sendWithoutNotification)) val tempID = System.currentTimeMillis().toInt()
dataStore.saveMessageQueue(roomToken, messageQueue) val qMsg = QueuedMessage(tempID, message, displayName, replyTo, sendWithoutNotification)
messageQueue = dataStore.getMessageQueue(internalId)
messageQueue.add(qMsg)
dataStore.saveMessageQueue(internalId, messageQueue)
_messageQueueSizeFlow.update { messageQueue.size } _messageQueueSizeFlow.update { messageQueue.size }
_messageQueueFlow.postValue(listOf(qMsg))
return return
} }
@ -242,17 +251,16 @@ class MessageInputViewModel @Inject constructor(
_getRecordingTime.postValue(time) _getRecordingTime.postValue(time)
} }
fun sendAndEmptyMessageQueue(roomToken: String, credentials: String, url: String) { fun sendAndEmptyMessageQueue(internalId: String, credentials: String, url: String) {
if (isQueueing) return if (isQueueing) return
messageQueue.clear() messageQueue.clear()
val queue = dataStore.getMessageQueue(roomToken) val queue = dataStore.getMessageQueue(internalId)
dataStore.saveMessageQueue(roomToken, null) // empties the queue dataStore.saveMessageQueue(internalId, null) // empties the queue
while (queue.size > 0) { while (queue.size > 0) {
val msg = queue.removeFirst() val msg = queue.removeAt(0)
sleep(DELAY_BETWEEN_QUEUED_MESSAGES)
sendChatMessage( sendChatMessage(
roomToken, internalId,
credentials, credentials,
url, url,
msg.message!!, msg.message!!,
@ -260,20 +268,55 @@ class MessageInputViewModel @Inject constructor(
msg.replyTo!!, msg.replyTo!!,
msg.sendWithoutNotification!! 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) { fun switchToMessageQueue(shouldQueue: Boolean) {
isQueueing = shouldQueue isQueueing = shouldQueue
} }
fun restoreMessageQueue(roomToken: String) { fun restoreMessageQueue(internalId: String) {
messageQueue = dataStore.getMessageQueue(roomToken) messageQueue = dataStore.getMessageQueue(internalId)
_messageQueueSizeFlow.tryEmit(messageQueue.size) _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 { companion object {
private val TAG = MessageInputViewModel::class.java.simpleName 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
} }
} }

View file

@ -20,6 +20,7 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -73,6 +74,7 @@ class NetworkMonitorImpl @Inject constructor(
} }
} }
} }
.distinctUntilChanged()
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.conflate() .conflate()

View file

@ -25,6 +25,7 @@ import com.nextcloud.talk.models.json.generic.GenericOverall;
import com.nextcloud.talk.models.json.push.PushConfigurationState; import com.nextcloud.talk.models.json.push.PushConfigurationState;
import com.nextcloud.talk.users.UserManager; import com.nextcloud.talk.users.UserManager;
import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import com.nextcloud.talk.webrtc.WebSocketConnectionHelper; import com.nextcloud.talk.webrtc.WebSocketConnectionHelper;
import java.net.CookieManager; import java.net.CookieManager;
@ -53,6 +54,8 @@ public class AccountRemovalWorker extends Worker {
@Inject ArbitraryStorageManager arbitraryStorageManager; @Inject ArbitraryStorageManager arbitraryStorageManager;
@Inject AppPreferences appPreferences;
@Inject Retrofit retrofit; @Inject Retrofit retrofit;
@Inject OkHttpClient okHttpClient; @Inject OkHttpClient okHttpClient;
@ -193,6 +196,7 @@ public class AccountRemovalWorker extends Worker {
if (user.getId() != null) { if (user.getId() != null) {
String username = user.getUsername(); String username = user.getUsername();
try { try {
appPreferences.deleteAllMessageQueuesFor(user.getUserId());
userManager.deleteUser(user.getId()); userManager.deleteUser(user.getId());
Log.d(TAG, "deleted user: " + username); Log.d(TAG, "deleted user: " + username);
} catch (Throwable e) { } catch (Throwable e) {

View file

@ -176,6 +176,8 @@ public interface AppPreferences {
List<MessageInputViewModel.QueuedMessage> getMessageQueue(String internalConversationId); List<MessageInputViewModel.QueuedMessage> getMessageQueue(String internalConversationId);
void deleteAllMessageQueuesFor(String userId);
void clear(); void clear();
} }

View file

@ -484,7 +484,10 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
var queueStr = "" var queueStr = ""
queue?.let { queue?.let {
for (msg in queue) { 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 queueStr += msgStr
} }
} }
@ -504,12 +507,13 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
try { try {
if (msgStr.isNotEmpty()) { if (msgStr.isNotEmpty()) {
val msgArray = msgStr.split(",") val msgArray = msgStr.split(",")
val id = msgArray[ID].toInt()
val message = msgArray[MESSAGE_INDEX] val message = msgArray[MESSAGE_INDEX]
val replyTo = msgArray[REPLY_TO_INDEX].toInt() 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 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) queue.add(qMsg)
} }
} catch (e: IndexOutOfBoundsException) { } catch (e: IndexOutOfBoundsException) {
@ -520,6 +524,26 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
return queue 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() {} override fun clear() {}
private suspend fun writeString(key: String, value: String) = private suspend fun writeString(key: String, value: String) =
@ -572,10 +596,11 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
@Suppress("UnusedPrivateProperty") @Suppress("UnusedPrivateProperty")
private val TAG = AppPreferencesImpl::class.simpleName private val TAG = AppPreferencesImpl::class.simpleName
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
private const val MESSAGE_INDEX: Int = 0 private const val ID: Int = 0
private const val REPLY_TO_INDEX: Int = 1 private const val MESSAGE_INDEX: Int = 1
private const val DISPLY_NAME_INDEX: Int = 2 private const val REPLY_TO_INDEX: Int = 2
private const val SILENT_INDEX: Int = 3 private const val DISPLAY_NAME_INDEX: Int = 3
private const val SILENT_INDEX: Int = 4
const val PROXY_TYPE = "proxy_type" const val PROXY_TYPE = "proxy_type"
const val PROXY_SERVER = "proxy_server" const val PROXY_SERVER = "proxy_server"
const val PROXY_HOST = "proxy_host" const val PROXY_HOST = "proxy_host"

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