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 816a9d760d..b542b98449 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -7,6 +7,7 @@ package com.nextcloud.talk.chat +import android.content.Context import android.content.res.Resources import android.os.Build import android.os.Bundle @@ -28,6 +29,7 @@ import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.Animation.AnimationListener import android.view.animation.LinearInterpolator +import android.view.inputmethod.InputMethodManager import android.widget.Chronometer import android.widget.ImageButton import android.widget.ImageView @@ -37,9 +39,11 @@ import android.widget.RelativeLayout import android.widget.SeekBar import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.material3.MaterialTheme +import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toDrawable import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged +import androidx.emoji2.emojipicker.RecentEmojiProvider import androidx.emoji2.widget.EmojiTextView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -117,6 +121,7 @@ class MessageInputFragment : Fragment() { private var typedWhileTypingTimerIsRunning: Boolean = false private var typingTimer: CountDownTimer? = null private lateinit var chatActivity: ChatActivity + private var restoreKeyboardOnEmojiDismiss = false private var mentionAutocomplete: Autocomplete<*>? = null private var xcounter = 0f private var ycounter = 0f @@ -147,6 +152,7 @@ class MessageInputFragment : Fragment() { } override fun onDestroyView() { + restoreKeyboardOnEmojiDismiss = false super.onDestroyView() if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { mentionAutocomplete?.dismissPopup() @@ -179,6 +185,7 @@ class MessageInputFragment : Fragment() { when (state) { is ChatViewModel.GetCapabilitiesUpdateState -> { initMessageInputView(state.spreedCapabilities) + initSmileyKeyboardToggler() setupMentionAutocomplete() initVoiceRecordButton() initThreadHandling() @@ -188,6 +195,7 @@ class MessageInputFragment : Fragment() { is ChatViewModel.GetCapabilitiesInitialLoadState -> { initMessageInputView(state.spreedCapabilities) + initSmileyKeyboardToggler() setupMentionAutocomplete() initVoiceRecordButton() initThreadHandling() @@ -773,6 +781,8 @@ class MessageInputFragment : Fragment() { private fun showRecordAudioUi(show: Boolean) { if (show) { + restoreKeyboardOnEmojiDismiss = false + binding.emojiPicker.isVisible = false val animation: Animation = AlphaAnimation(FULLY_OPAQUE, FULLY_TRANSPARENT) animation.duration = ANIMATION_DURATION animation.interpolator = LinearInterpolator() @@ -785,6 +795,7 @@ class MessageInputFragment : Fragment() { binding.fragmentMessageInputView.audioRecordDuration.visibility = View.VISIBLE binding.fragmentMessageInputView.slideToCancelDescription.visibility = View.VISIBLE binding.fragmentMessageInputView.attachmentButton.visibility = View.GONE + binding.fragmentMessageInputView.smileyButton.visibility = View.GONE binding.fragmentMessageInputView.messageInput.visibility = View.GONE binding.fragmentMessageInputView.messageInput.hint = "" binding.fragmentMessageInputView.scheduledMessagesButton.visibility = View.GONE @@ -796,12 +807,95 @@ class MessageInputFragment : Fragment() { binding.fragmentMessageInputView.audioRecordDuration.visibility = View.GONE binding.fragmentMessageInputView.slideToCancelDescription.visibility = View.GONE binding.fragmentMessageInputView.attachmentButton.visibility = View.VISIBLE + binding.fragmentMessageInputView.smileyButton.visibility = View.VISIBLE binding.fragmentMessageInputView.messageInput.visibility = View.VISIBLE binding.fragmentMessageInputView.messageInput.hint = requireContext().resources?.getString(R.string.nc_hint_enter_a_message) } } + private fun updateSmileyButtonIcon() { + val icon = if (binding.emojiPicker.isVisible) { + R.drawable.ic_baseline_keyboard_24 + } else { + R.drawable.ic_insert_emoticon_black_24dp + } + val drawable = ContextCompat.getDrawable(requireContext(), icon) + binding.fragmentMessageInputView.smileyButton.setImageDrawable(drawable) + } + + private fun showKeyboard() { + val editText = binding.fragmentMessageInputView.inputEditText + editText.requestFocus() + val inputMethodManager = + requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + inputMethodManager?.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) + } + + private fun hideKeyboard() { + val view = activity?.currentFocus + if (view != null) { + val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.hideSoftInputFromWindow(view.windowToken, 0) + } + } + + private fun initSmileyKeyboardToggler() { + val inputEditText = binding.fragmentMessageInputView.inputEditText + val emojiPicker = binding.emojiPicker + + emojiPicker.setRecentEmojiProvider(MessageInputRecentEmojiProvider(requireContext())) + emojiPicker.setOnEmojiPickedListener { item -> + val start = inputEditText.selectionStart + val end = inputEditText.selectionEnd + inputEditText.editableText.replace(start, end, item.emoji) + } + + viewThemeUtils.talk.themeEmojiPicker(emojiPicker) + updateSmileyButtonIcon() + + binding.fragmentMessageInputView.smileyButton.setOnClickListener { + if (emojiPicker.isVisible) { + showKeyboard() + emojiPicker.isVisible = false + } else { + hideKeyboard() + emojiPicker.isVisible = true + } + updateSmileyButtonIcon() + } + + inputEditText.setOnClickListener { + if (emojiPicker.isVisible) { + emojiPicker.isVisible = false + updateSmileyButtonIcon() + } + } + } + + private class MessageInputRecentEmojiProvider(context: Context) : RecentEmojiProvider { + private val prefs = context.getSharedPreferences("recent_emojis", Context.MODE_PRIVATE) + + override fun recordSelection(emoji: String) { + val updated = listOf(emoji) + getStoredList().filterNot { it == emoji } + prefs.edit() + .putString("recent", updated.take(MAX_STORED_RECENT_EMOJIS).joinToString(",")) + .apply() + } + + override suspend fun getRecentEmojiList(): List = getStoredList() + + private fun getStoredList(): List = + prefs.getString("recent", null) + ?.split(",") + ?.filter { it.isNotBlank() } + ?: emptyList() + + companion object { + private const val MAX_STORED_RECENT_EMOJIS = 20 + } + } + private fun replyToMessage(quotedMessageText: String?, quotedActorDisplayName: String?, quotedImageUrl: String?) { Log.d(TAG, "Reply") val view = binding.fragmentMessageInputView diff --git a/app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt b/app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt index 5c30b38528..eaad8c81d0 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt @@ -26,6 +26,7 @@ class MessageInput : FrameLayout { lateinit var slideToCancelDescription: TextView lateinit var microphoneEnabledInfo: ImageView lateinit var microphoneEnabledInfoBackground: ImageView + lateinit var smileyButton: ImageButton lateinit var deleteVoiceRecording: ImageView lateinit var sendVoiceRecording: ImageView lateinit var micInputCloud: MicInputCloud @@ -70,6 +71,7 @@ class MessageInput : FrameLayout { slideToCancelDescription = findViewById(R.id.slideToCancelDescription) microphoneEnabledInfo = findViewById(R.id.microphoneEnabledInfo) microphoneEnabledInfoBackground = findViewById(R.id.microphoneEnabledInfoBackground) + smileyButton = findViewById(R.id.smileyButton) deleteVoiceRecording = findViewById(R.id.deleteVoiceRecording) sendVoiceRecording = findViewById(R.id.sendVoiceRecording) micInputCloud = findViewById(R.id.micInputCloud) diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt index 54c8334078..edcda2624f 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt @@ -35,6 +35,7 @@ import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.ColorUtils import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.ViewCompat +import androidx.emoji2.emojipicker.EmojiPickerView import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView import com.google.android.material.chip.Chip @@ -492,6 +493,18 @@ class TalkSpecificViewThemeUtils @Inject constructor( } } + fun themeEmojiPicker(emojiPickerView: EmojiPickerView) { + withScheme(emojiPickerView.context) { scheme -> + emojiPickerView.setBackgroundColor(dynamicColor.surfaceContainerLow().getArgb(scheme)) + themeEmojiPickerCategoryTabs( + emojiPickerView, + dynamicColor.primary().getArgb(scheme), + dynamicColor.onSurfaceVariant().getArgb(scheme) + ) + protectEmojiPickerScrollGesture(emojiPickerView) + } + } + companion object { private val THEMEABLE_PLACEHOLDER_IDS = listOf( R.drawable.ic_mimetype_package_x_generic, diff --git a/app/src/main/res/layout/fragment_message_input.xml b/app/src/main/res/layout/fragment_message_input.xml index 189eb5f845..aec5fc6753 100644 --- a/app/src/main/res/layout/fragment_message_input.xml +++ b/app/src/main/res/layout/fragment_message_input.xml @@ -81,6 +81,12 @@ android:textColor="@color/medium_emphasis_text" android:visibility="gone" tools:visibility="visible" /> + + diff --git a/app/src/main/res/layout/view_message_input.xml b/app/src/main/res/layout/view_message_input.xml index 1aa188e06e..34f2fb6dd4 100644 --- a/app/src/main/res/layout/view_message_input.xml +++ b/app/src/main/res/layout/view_message_input.xml @@ -44,13 +44,25 @@ android:src="@drawable/ic_baseline_attach_file_24" app:tint="?attr/colorControlNormal" /> + +