From 666c4c9853b8ff71cf2f04e46ef78f2a20f555e9 Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Thu, 5 Jun 2025 16:38:35 -0500 Subject: [PATCH 1/3] Allow replies to maintain state on orientation change Signed-off-by: rapterjet2004 --- .../talk/chat/MessageInputFragment.kt | 148 ++++++++++++------ 1 file changed, 99 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt index c6041ccd4..c30746c78 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -79,7 +79,6 @@ import com.nextcloud.talk.utils.SpreedFeatures import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew 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.flow.collect import kotlinx.coroutines.flow.onEach @@ -87,7 +86,7 @@ import kotlinx.coroutines.launch import java.util.Objects import javax.inject.Inject -@Suppress("LongParameterList", "TooManyFunctions") +@Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "LongMethod") @AutoInjector(NextcloudTalkApplication::class) class MessageInputFragment : Fragment() { @@ -110,6 +109,10 @@ class MessageInputFragment : Fragment() { private const val CONNECTION_ESTABLISHED_ANIM_DURATION: Long = 3000 private const val FULLY_OPAQUE: Float = 1.0f private const val FULLY_TRANSPARENT: Float = 0.0f + private const val QUOTED_MESSAGE_TEXT = "QUOTED_MESSAGE_TEXT" + private const val QUOTED_MESSAGE_ID = "QUOTED_MESSAGE_ID" + private const val QUOTED_MESSAGE_URL = "QUOTED_MESSAGE_URL" + private const val QUOTED_MESSAGE_NAME = "QUOTED_MESSAGE_NAME" } @Inject @@ -134,6 +137,10 @@ class MessageInputFragment : Fragment() { private var xcounter = 0f private var ycounter = 0f private var collapsed = false + private var quotedMessageText: String? = "" + private var quotedActorDisplayName: String? = null + private var quotedImageUrl: String? = null + private var quotedJsonId: Int = -1 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -157,8 +164,8 @@ class MessageInputFragment : Fragment() { } override fun onPause() { - super.onPause() saveState() + super.onPause() } override fun onDestroyView() { @@ -167,7 +174,10 @@ class MessageInputFragment : Fragment() { mentionAutocomplete?.dismissPopup() } clearEditUI() - cancelReply() + val isInReplyState = (quotedJsonId != -1 && quotedActorDisplayName != null && quotedMessageText != "") + if (!isInReplyState) { + cancelReply() + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -178,7 +188,13 @@ class MessageInputFragment : Fragment() { private fun initObservers() { Log.d(TAG, "LifeCyclerOwner is: ${viewLifecycleOwner.lifecycle}") chatActivity.messageInputViewModel.getReplyChatMessage.observe(viewLifecycleOwner) { message -> - message?.let { replyToMessage(message) } + (message as ChatMessage?)?.let { + quotedMessageText = message.text + quotedActorDisplayName = message.actorDisplayName + quotedImageUrl = message.imageUrl + quotedJsonId = message.jsonMessageId + replyToMessage(message.text, message.actorDisplayName, message.imageUrl, message.jsonMessageId) + } } chatActivity.messageInputViewModel.getEditChatMessage.observe(viewLifecycleOwner) { message -> @@ -300,6 +316,14 @@ class MessageInputFragment : Fragment() { val cursor = getInt(chatActivity.roomToken + CURSOR_KEY, 0) binding.fragmentMessageInputView.messageInput.setText(text) binding.fragmentMessageInputView.messageInput.setSelection(cursor) + quotedJsonId = getInt(QUOTED_MESSAGE_ID, -1) + quotedImageUrl = getString(QUOTED_MESSAGE_URL, null) + quotedMessageText = getString(QUOTED_MESSAGE_TEXT, "") + quotedActorDisplayName = getString(QUOTED_MESSAGE_NAME, null) + val isInReplyState = (quotedJsonId != -1 && quotedActorDisplayName != null && quotedMessageText != "") + if (isInReplyState) { + replyToMessage(quotedMessageText, quotedActorDisplayName, quotedImageUrl, quotedJsonId) + } } } } @@ -313,15 +337,23 @@ class MessageInputFragment : Fragment() { .MODE_PRIVATE ).getString(chatActivity.roomToken, "null") - if (text != previous) { - requireContext().getSharedPreferences( - chatActivity.localClassName, - AppCompatActivity.MODE_PRIVATE - ).edit().apply { + val isInReplyState = (quotedJsonId != -1 && quotedActorDisplayName != null && quotedMessageText != "") + requireContext().getSharedPreferences( + chatActivity.localClassName, + AppCompatActivity.MODE_PRIVATE + ).edit().apply { + if (text != previous) { putString(chatActivity.roomToken, text) putInt(chatActivity.roomToken + CURSOR_KEY, cursor) - apply() } + + if (isInReplyState) { + putInt(QUOTED_MESSAGE_ID, quotedJsonId) + putString(QUOTED_MESSAGE_NAME, quotedActorDisplayName) + putString(QUOTED_MESSAGE_TEXT, quotedMessageText) + putString(QUOTED_MESSAGE_URL, quotedImageUrl) // may be null + } + apply() } } @@ -598,7 +630,7 @@ class MessageInputFragment : Fragment() { } } } - v?.onTouchEvent(event) ?: true + v?.onTouchEvent(event) != false } } @@ -700,52 +732,54 @@ class MessageInputFragment : Fragment() { } } - private fun replyToMessage(message: IMessage?) { + private fun replyToMessage( + quotedMessageText: String?, + quotedActorDisplayName: String?, + quotedImageUrl: String?, + quotedJsonId: Int + ) { Log.d(TAG, "Reply") - val chatMessage = message as ChatMessage? - chatMessage?.let { - val view = binding.fragmentMessageInputView - view.findViewById(R.id.attachmentButton)?.visibility = - View.GONE - view.findViewById(R.id.cancelReplyButton)?.visibility = - View.VISIBLE + val view = binding.fragmentMessageInputView + view.findViewById(R.id.attachmentButton)?.visibility = + View.GONE + view.findViewById(R.id.cancelReplyButton)?.visibility = + View.VISIBLE - val quotedMessage = view.findViewById(R.id.quotedMessage) + val quotedMessage = view.findViewById(R.id.quotedMessage) - quotedMessage?.maxLines = 2 - quotedMessage?.ellipsize = TextUtils.TruncateAt.END - quotedMessage?.text = it.text - view.findViewById(R.id.quotedMessageAuthor)?.text = - it.actorDisplayName ?: requireContext().getText(R.string.nc_nick_guest) + quotedMessage?.maxLines = 2 + quotedMessage?.ellipsize = TextUtils.TruncateAt.END + quotedMessage?.text = quotedMessageText + view.findViewById(R.id.quotedMessageAuthor)?.text = + quotedActorDisplayName ?: requireContext().getText(R.string.nc_nick_guest) - chatActivity.conversationUser?.let { - val quotedMessageImage = view.findViewById(R.id.quotedMessageImage) - chatMessage.imageUrl?.let { previewImageUrl -> - quotedMessageImage?.visibility = View.VISIBLE + chatActivity.conversationUser?.let { + val quotedMessageImage = view.findViewById(R.id.quotedMessageImage) + quotedImageUrl.let { previewImageUrl -> + quotedMessageImage?.visibility = View.VISIBLE - val px = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - QUOTED_MESSAGE_IMAGE_MAX_HEIGHT, - resources.displayMetrics - ) + val px = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + QUOTED_MESSAGE_IMAGE_MAX_HEIGHT, + resources.displayMetrics + ) - quotedMessageImage?.maxHeight = px.toInt() - val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams - layoutParams.flexGrow = 0f - quotedMessageImage.layoutParams = layoutParams - quotedMessageImage.load(previewImageUrl) { - addHeader("Authorization", chatActivity.credentials!!) - } - } ?: run { - view.findViewById(R.id.quotedMessageImage)?.visibility = View.GONE + quotedMessageImage?.maxHeight = px.toInt() + val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams + layoutParams.flexGrow = 0f + quotedMessageImage.layoutParams = layoutParams + quotedMessageImage.load(previewImageUrl) { + addHeader("Authorization", chatActivity.credentials!!) } + } ?: run { + view.findViewById(R.id.quotedMessageImage)?.visibility = View.GONE } - - val quotedChatMessageView = - view.findViewById(R.id.quotedChatMessageView) - quotedChatMessageView?.tag = message?.jsonMessageId - quotedChatMessageView?.visibility = View.VISIBLE } + + val quotedChatMessageView = + view.findViewById(R.id.quotedChatMessageView) + quotedChatMessageView?.tag = quotedJsonId + quotedChatMessageView?.visibility = View.VISIBLE } fun updateOwnTypingStatus(typedText: CharSequence) { @@ -1020,5 +1054,21 @@ class MessageInputFragment : Fragment() { quote.tag = null binding.fragmentMessageInputView.findViewById(R.id.attachmentButton)?.visibility = View.VISIBLE chatActivity.messageInputViewModel.reply(null) + + quotedMessageText = "" + quotedActorDisplayName = null + quotedImageUrl = null + quotedJsonId = -1 + + requireContext().getSharedPreferences( + chatActivity.localClassName, + AppCompatActivity.MODE_PRIVATE + ).edit().apply { + putInt(QUOTED_MESSAGE_ID, quotedJsonId) + putString(QUOTED_MESSAGE_NAME, quotedActorDisplayName) + putString(QUOTED_MESSAGE_TEXT, quotedMessageText) + putString(QUOTED_MESSAGE_URL, quotedImageUrl) // may be null + apply() + } } } From 46edec93b2e53c828f0ffcfca0266a05206e7c7f Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Tue, 17 Jun 2025 11:18:18 -0500 Subject: [PATCH 2/3] clears the state when returning to ConversationsListActivity.kt Signed-off-by: rapterjet2004 --- .../talk/chat/MessageInputFragment.kt | 12 +++++---- .../ConversationsListActivity.kt | 25 +++++++++++++++---- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt index c30746c78..c3ec88d5f 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -109,10 +109,10 @@ class MessageInputFragment : Fragment() { private const val CONNECTION_ESTABLISHED_ANIM_DURATION: Long = 3000 private const val FULLY_OPAQUE: Float = 1.0f private const val FULLY_TRANSPARENT: Float = 0.0f - private const val QUOTED_MESSAGE_TEXT = "QUOTED_MESSAGE_TEXT" - private const val QUOTED_MESSAGE_ID = "QUOTED_MESSAGE_ID" - private const val QUOTED_MESSAGE_URL = "QUOTED_MESSAGE_URL" - private const val QUOTED_MESSAGE_NAME = "QUOTED_MESSAGE_NAME" + const val QUOTED_MESSAGE_TEXT = "QUOTED_MESSAGE_TEXT" + const val QUOTED_MESSAGE_ID = "QUOTED_MESSAGE_ID" + const val QUOTED_MESSAGE_URL = "QUOTED_MESSAGE_URL" + const val QUOTED_MESSAGE_NAME = "QUOTED_MESSAGE_NAME" } @Inject @@ -176,7 +176,8 @@ class MessageInputFragment : Fragment() { clearEditUI() val isInReplyState = (quotedJsonId != -1 && quotedActorDisplayName != null && quotedMessageText != "") if (!isInReplyState) { - cancelReply() + cancelReply() // TODO - I could move this to the view model, in a onBackPressCallback to remove all from + // storage } } @@ -311,6 +312,7 @@ class MessageInputFragment : Fragment() { private fun restoreState() { if (binding.fragmentMessageInputView.inputEditText.text.isEmpty()) { + Log.d("Julius", "State restored from: ${chatActivity.localClassName}") requireContext().getSharedPreferences(chatActivity.localClassName, AppCompatActivity.MODE_PRIVATE).apply { val text = getString(chatActivity.roomToken, "") val cursor = getInt(chatActivity.roomToken + CURSOR_KEY, 0) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 1086d9d8e..70e51104a 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -83,6 +83,10 @@ import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.MessageInputFragment.Companion.QUOTED_MESSAGE_ID +import com.nextcloud.talk.chat.MessageInputFragment.Companion.QUOTED_MESSAGE_NAME +import com.nextcloud.talk.chat.MessageInputFragment.Companion.QUOTED_MESSAGE_TEXT +import com.nextcloud.talk.chat.MessageInputFragment.Companion.QUOTED_MESSAGE_URL import com.nextcloud.talk.chat.viewmodels.ChatViewModel import com.nextcloud.talk.contacts.ContactsActivity import com.nextcloud.talk.contacts.ContactsUiState @@ -304,6 +308,16 @@ class ConversationsListActivity : showNotificationWarning() showShareToScreen = hasActivityActionSendIntent() + context.getSharedPreferences( + CHAT_ACTIVITY_LOCAL_NAME, + MODE_PRIVATE + ).edit().apply { + putInt(QUOTED_MESSAGE_ID, -1) + putString(QUOTED_MESSAGE_NAME, null) + putString(QUOTED_MESSAGE_TEXT, "") + putString(QUOTED_MESSAGE_URL, null) + apply() + } if (!eventBus.isRegistered(this)) { eventBus.register(this) @@ -640,7 +654,7 @@ class ConversationsListActivity : } } - val archiveFilterOn = filterState[ARCHIVE] ?: false + val archiveFilterOn = filterState[ARCHIVE] == true if (archiveFilterOn && newItems.isEmpty()) { binding.noArchivedConversationLayout.visibility = View.VISIBLE } else { @@ -755,7 +769,7 @@ class ConversationsListActivity : } private fun initSearchView() { - val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager? + val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager? if (searchItem != null) { searchView = MenuItemCompat.getActionView(searchItem) as SearchView viewThemeUtils.talk.themeSearchView(searchView!!) @@ -1217,7 +1231,7 @@ class ConversationsListActivity : }) binding.recyclerView.setOnTouchListener { v: View, _: MotionEvent? -> if (!isDestroyed) { - val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(v.windowToken, 0) } false @@ -1398,7 +1412,7 @@ class ConversationsListActivity : adapter?.updateDataSet(conversationItems) adapter?.setFilter("") adapter?.filterItems() - val archiveFilterOn = filterState[ARCHIVE] ?: false + val archiveFilterOn = filterState[ARCHIVE] == true if (archiveFilterOn && adapter!!.isEmpty) { binding.noArchivedConversationLayout.visibility = View.VISIBLE } else { @@ -1809,7 +1823,7 @@ class ConversationsListActivity : val callsChannelNotEnabled = !NotificationUtils.isCallsNotificationChannelEnabled(this) val serverNotificationAppInstalled = - currentUser?.capabilities?.notificationsCapability?.features?.isNotEmpty() ?: false + currentUser?.capabilities?.notificationsCapability?.features?.isNotEmpty() == true val settingsOfUserAreWrong = notificationPermissionNotGranted || batteryOptimizationNotIgnored || @@ -2167,6 +2181,7 @@ class ConversationsListActivity : const val UNREAD_BUBBLE_DELAY = 2500 const val BOTTOM_SHEET_DELAY: Long = 2500 private const val KEY_SEARCH_QUERY = "ConversationsListActivity.searchQuery" + private const val CHAT_ACTIVITY_LOCAL_NAME = "com.nextcloud.talk.chat.ChatActivity" const val SEARCH_DEBOUNCE_INTERVAL_MS = 300 const val SEARCH_MIN_CHARS = 1 const val HTTP_UNAUTHORIZED = 401 From 4b44a939f5258c48fa177cf1b4a8eea04db53a5b Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Tue, 17 Jun 2025 11:29:58 -0500 Subject: [PATCH 3/3] linter Signed-off-by: rapterjet2004 --- .../main/java/com/nextcloud/talk/chat/MessageInputFragment.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt index c3ec88d5f..5d2e87ba2 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -176,8 +176,7 @@ class MessageInputFragment : Fragment() { clearEditUI() val isInReplyState = (quotedJsonId != -1 && quotedActorDisplayName != null && quotedMessageText != "") if (!isInReplyState) { - cancelReply() // TODO - I could move this to the view model, in a onBackPressCallback to remove all from - // storage + cancelReply() } }