diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a48bf9c3a71..deed324b16d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -216,11 +216,11 @@ dependencies { implementation("androidx.appcompat:appcompat:1.7.1") implementation("com.google.android.material:material:1.14.0") implementation("androidx.constraintlayout:constraintlayout:2.2.1") - implementation("com.vanniktech:emoji-google:0.23.0") implementation("androidx.emoji2:emoji2:$emojiVersion") implementation("androidx.emoji2:emoji2-bundled:$emojiVersion") implementation("androidx.emoji2:emoji2-views:$emojiVersion") implementation("androidx.emoji2:emoji2-views-helper:$emojiVersion") + implementation("androidx.emoji2:emoji2-emojipicker:$emojiVersion") implementation("org.michaelevans.colorart:library:0.0.3") implementation("androidx.work:work-runtime:$workVersion") implementation("androidx.work:work-rxjava2:$workVersion") diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt index e46496ef564..56c8331a479 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt +++ b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt @@ -61,8 +61,6 @@ import com.nextcloud.talk.utils.NotificationUtils import com.nextcloud.talk.utils.database.arbitrarystorage.ArbitraryStorageModule import com.nextcloud.talk.utils.database.user.UserModule import com.nextcloud.talk.utils.preferences.AppPreferences -import com.vanniktech.emoji.EmojiManager -import com.vanniktech.emoji.google.GoogleEmojiProvider import de.cotech.hw.SecurityKeyManager import de.cotech.hw.SecurityKeyManagerConfig import io.reactivex.exceptions.UndeliverableException @@ -238,8 +236,6 @@ class NextcloudTalkApplication : config.setReplaceAll(true) val emojiCompat = EmojiCompat.init(config) - EmojiManager.install(GoogleEmojiProvider()) - NotificationUtils.registerNotificationChannels(applicationContext, appPreferences) } diff --git a/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt b/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt index 2ae09c6b271..41ad4e9ffdd 100644 --- a/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt +++ b/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt @@ -21,9 +21,9 @@ import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat +import androidx.emoji2.widget.EmojiTextView import com.nextcloud.talk.R import com.nextcloud.talk.ui.theme.ViewThemeUtils -import com.vanniktech.emoji.EmojiTextView class ReactionAnimator( val context: Context, diff --git a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java index c7ed9b3d966..a4ab6f80c96 100644 --- a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java +++ b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java @@ -12,6 +12,9 @@ import android.text.Spanned; import android.widget.EditText; +import androidx.emoji2.text.EmojiCompat; +import androidx.emoji2.text.EmojiSpan; + import com.nextcloud.talk.R; import com.nextcloud.talk.data.user.model.User; import com.nextcloud.talk.models.json.mention.Mention; @@ -20,9 +23,10 @@ import com.nextcloud.talk.utils.CharPolicy; import com.nextcloud.talk.utils.text.Spans; import com.otaliastudios.autocomplete.AutocompleteCallback; -import com.vanniktech.emoji.EmojiRange; -import com.vanniktech.emoji.Emojis; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Objects; import kotlin.OptIn; @@ -54,8 +58,21 @@ public boolean onPopupItemClicked(Editable editable, Mention item) { String replacement = item.getLabel(); StringBuilder replacementStringBuilder = new StringBuilder(Objects.requireNonNull(item.getLabel())); - for (EmojiRange emojiRange : Emojis.emojis(replacement)) { - replacementStringBuilder.delete(emojiRange.range.getStart(), emojiRange.range.getEndInclusive()); + final EmojiCompat emojiCompat = EmojiCompat.get(); + if (emojiCompat.getLoadState() == EmojiCompat.LOAD_STATE_SUCCEEDED) { + final CharSequence processed = emojiCompat.process(replacement); + if (processed instanceof Spanned) { + final Spanned spannedReplacement = (Spanned) processed; + final EmojiSpan[] emojiSpans = + spannedReplacement.getSpans(0, spannedReplacement.length(), EmojiSpan.class); + final List sortedSpans = new ArrayList<>(Arrays.asList(emojiSpans)); + sortedSpans.sort((a, b) -> spannedReplacement.getSpanStart(b) - spannedReplacement.getSpanStart(a)); + for (EmojiSpan emojiSpan : sortedSpans) { + replacementStringBuilder.delete( + spannedReplacement.getSpanStart(emojiSpan), + spannedReplacement.getSpanEnd(emojiSpan)); + } + } } String charSequence = " "; 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 93c79a90b8e..9dbf2600bfe 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -7,7 +7,6 @@ package com.nextcloud.talk.chat -import android.content.Context import android.content.res.Resources import android.graphics.drawable.Drawable import android.os.Build @@ -30,7 +29,6 @@ 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 @@ -40,7 +38,6 @@ import android.widget.RelativeLayout import android.widget.SeekBar import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.view.ContextThemeWrapper -import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toDrawable import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged @@ -92,7 +89,6 @@ import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.text.Spans import com.otaliastudios.autocomplete.Autocomplete -import com.vanniktech.emoji.EmojiPopup import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach @@ -129,9 +125,6 @@ class MessageInputFragment : Fragment() { private var typedWhileTypingTimerIsRunning: Boolean = false private var typingTimer: CountDownTimer? = null private lateinit var chatActivity: ChatActivity - private var emojiPopup: EmojiPopup? = null - private var isEmojiPopupOpen = false - private var restoreKeyboardOnEmojiDismiss = false private var mentionAutocomplete: Autocomplete<*>? = null private var xcounter = 0f private var ycounter = 0f @@ -163,10 +156,6 @@ class MessageInputFragment : Fragment() { } override fun onDestroyView() { - restoreKeyboardOnEmojiDismiss = false - emojiPopup?.dismiss() - emojiPopup = null - isEmojiPopupOpen = false super.onDestroyView() if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { mentionAutocomplete?.dismissPopup() @@ -193,7 +182,6 @@ class MessageInputFragment : Fragment() { when (state) { is ChatViewModel.GetCapabilitiesUpdateState -> { initMessageInputView(state.spreedCapabilities) - initSmileyKeyboardToggler() setupMentionAutocomplete() initVoiceRecordButton() initThreadHandling() @@ -202,7 +190,6 @@ class MessageInputFragment : Fragment() { is ChatViewModel.GetCapabilitiesInitialLoadState -> { initMessageInputView(state.spreedCapabilities) - initSmileyKeyboardToggler() setupMentionAutocomplete() initVoiceRecordButton() initThreadHandling() @@ -827,8 +814,6 @@ class MessageInputFragment : Fragment() { private fun showRecordAudioUi(show: Boolean) { if (show) { - restoreKeyboardOnEmojiDismiss = false - emojiPopup?.dismiss() val animation: Animation = AlphaAnimation(FULLY_OPAQUE, FULLY_TRANSPARENT) animation.duration = ANIMATION_DURATION animation.interpolator = LinearInterpolator() @@ -841,7 +826,6 @@ 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 @@ -853,70 +837,12 @@ 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 (isEmojiPopupOpen) { - 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 initSmileyKeyboardToggler() { - val inputEditText = binding.fragmentMessageInputView.inputEditText - if (emojiPopup == null) { - emojiPopup = EmojiPopup( - rootView = binding.root, - editText = inputEditText, - theming = viewThemeUtils.talk.getEmojiTheming(requireContext()), - onEmojiPopupShownListener = { - isEmojiPopupOpen = true - updateSmileyButtonIcon() - }, - onEmojiPopupDismissListener = { - isEmojiPopupOpen = false - updateSmileyButtonIcon() - if (restoreKeyboardOnEmojiDismiss) { - restoreKeyboardOnEmojiDismiss = false - showKeyboard() - } - }, - onEmojiClickListener = { - inputEditText.editableText?.append(" ") - } - ) - } - - updateSmileyButtonIcon() - - binding.fragmentMessageInputView.smileyButton.setOnClickListener { - if (isEmojiPopupOpen) { - restoreKeyboardOnEmojiDismiss = true - emojiPopup?.dismiss() - } else { - restoreKeyboardOnEmojiDismiss = false - inputEditText.requestFocus() - emojiPopup?.toggle() - } - } - } - 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/chat/ScheduledMessagesActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt index 2970e3782b0..6f6f14708f0 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ScheduledMessagesActivity.kt @@ -52,7 +52,6 @@ import androidx.compose.material3.lightColorScheme import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -68,7 +67,6 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource @@ -83,6 +81,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.widget.addTextChangedListener +import androidx.emoji2.widget.EmojiEditText import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector @@ -107,15 +106,12 @@ import com.nextcloud.talk.ui.chat.LocalShowThreadButton import com.nextcloud.talk.ui.chat.formatTime import com.nextcloud.talk.ui.theme.LocalMessageUtils import com.nextcloud.talk.ui.theme.LocalViewThemeUtils -import com.nextcloud.talk.ui.theme.emojiTheming import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DateConstants import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID -import com.vanniktech.emoji.EmojiEditText -import com.vanniktech.emoji.EmojiPopup import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import java.time.Instant @@ -808,24 +804,14 @@ class ScheduledMessagesActivity : BaseActivity() { onCancel: () -> Unit, onSend: () -> Unit ) { - val rootView = LocalView.current val keyboardController = LocalSoftwareKeyboardController.current - val theming = emojiTheming() val emojiEditTextRef = remember { mutableStateOf(null) } - var emojiPopup by remember { mutableStateOf(null) } - var isEmojiOpen by remember { mutableStateOf(false) } val canSend = editValue.text.isNotBlank() && editValue.text.trim() != originalText.trim() - DisposableEffect(rootView) { - onDispose { - emojiPopup?.dismiss() - } - } - Surface(tonalElevation = 2.dp, modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(12.dp)) { Row( @@ -844,7 +830,6 @@ class ScheduledMessagesActivity : BaseActivity() { modifier = Modifier.weight(1f) ) IconButton(onClick = { - emojiPopup?.dismiss() keyboardController?.hide() onCancel() }) { @@ -867,37 +852,6 @@ class ScheduledMessagesActivity : BaseActivity() { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - IconButton(onClick = { - val editText = emojiEditTextRef.value ?: return@IconButton - - if (emojiPopup == null) { - emojiPopup = EmojiPopup( - rootView = rootView, - editText = editText, - theming = theming, - onEmojiPopupShownListener = { - isEmojiOpen = true - }, - onEmojiPopupDismissListener = { - isEmojiOpen = false - keyboardController?.show() - } - ) - } - - emojiPopup?.toggle() - }) { - Icon( - imageVector = if (isEmojiOpen) { - ImageVector.vectorResource(R.drawable.ic_baseline_keyboard_24) - } else { - ImageVector.vectorResource(R.drawable.ic_insert_emoticon_black_24dp) - }, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - AndroidView( modifier = Modifier .weight(1f) @@ -930,7 +884,6 @@ class ScheduledMessagesActivity : BaseActivity() { onClick = { if (!canSend) return@IconButton emojiEditTextRef.value?.clearFocus() - emojiPopup?.dismiss() keyboardController?.hide() onSend() }, diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt index 3a93ed1887d..a89538f59dd 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/MessageActionsBottomSheet.kt @@ -7,13 +7,10 @@ package com.nextcloud.talk.chat.ui -import android.annotation.SuppressLint import android.content.Context import android.content.res.Configuration -import android.os.Handler -import android.os.Looper -import android.view.MotionEvent -import android.view.inputmethod.InputMethodManager +import android.view.ContextThemeWrapper +import androidx.activity.compose.BackHandler import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints @@ -30,10 +27,13 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Surface @@ -43,79 +43,74 @@ import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat +import androidx.core.util.Consumer +import androidx.emoji2.emojipicker.EmojiPickerView +import androidx.emoji2.emojipicker.RecentEmojiProvider import com.nextcloud.talk.R import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.capabilities.SpreedCapability import com.nextcloud.talk.models.json.conversations.ConversationEnums -import com.nextcloud.talk.ui.theme.emojiTheming +import com.nextcloud.talk.ui.theme.themeEmojiPickerCategoryTabs import com.nextcloud.talk.utils.CapabilitiesUtil import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.DateConstants import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.SpreedFeatures -import com.vanniktech.emoji.Emoji -import com.vanniktech.emoji.EmojiEditText -import com.vanniktech.emoji.EmojiPopup -import com.vanniktech.emoji.installDisableKeyboardInput -import com.vanniktech.emoji.installForceSingleEmoji -import com.vanniktech.emoji.recent.RecentEmojiManager -import com.vanniktech.emoji.search.SearchEmojiManager import java.util.Date +import kotlinx.coroutines.launch private const val TAG = "MessageActionsSheet" private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE = 86400000L private const val AGE_THRESHOLD_FOR_DELETE_MESSAGE = 21600000L -private const val EMOJI_POPUP_TOGGLE_DELAY = 200L private const val MAX_RECENTS = 8 private const val ACTOR_BOTS = "bots" +private const val RECENT_REACTIONS_PREFS = "recent_reaction_emojis" +private const val RECENT_REACTIONS_KEY = "recent" +private const val MAX_STORED_RECENT_REACTIONS = 20 -private val emojiSearchKeywords = mapOf( - "👍" to "thumbsup", - "👎" to "thumbsdown", - "❤️" to "heart", - "😂" to "joy", - "😕" to "confused", - "😢" to "cry", - "🙏" to "pray", - "🔥" to "fire" -) +private val defaultReactionEmojis = listOf("👍", "👎", "❤️", "😂", "😕", "😢", "🙏", "🔥") +private val emojiPickerHeight = 360.dp -private fun buildEmojiList(recentEmojiManager: RecentEmojiManager): List { - val recentEmojis = recentEmojiManager.getRecentEmojis() - val searchEmojiManager = SearchEmojiManager() - val initialKeywords = listOf("thumbsup", "thumbsdown", "heart", "joy", "confused", "cry", "pray", "fire") - val initialEmojisFromSearch = mutableSetOf() - initialKeywords.forEach { keyword -> - val results = searchEmojiManager.search(keyword) - if (results.isNotEmpty()) { - initialEmojisFromSearch.add(results[0].component1()) - recentEmojiManager.addEmoji(results[0].component1()) - } - if (initialEmojisFromSearch.size >= MAX_RECENTS) return@forEach +private class ReactionRecentEmojiProvider(context: Context) : RecentEmojiProvider { + private val prefs = context.getSharedPreferences(RECENT_REACTIONS_PREFS, Context.MODE_PRIVATE) + + override fun recordSelection(emoji: String) { + val updated = listOf(emoji) + getStoredList().filterNot { it == emoji } + prefs.edit() + .putString(RECENT_REACTIONS_KEY, updated.take(MAX_STORED_RECENT_REACTIONS).joinToString(",")) + .apply() } - return (recentEmojis + initialEmojisFromSearch).distinct().take(MAX_RECENTS).map { it.unicode } + + override suspend fun getRecentEmojiList(): List = getStoredList() + + private fun getStoredList(): List = + prefs.getString(RECENT_REACTIONS_KEY, null) + ?.split(",") + ?.filter { it.isNotBlank() } + ?: emptyList() } data class MessageActionsState( @@ -296,34 +291,62 @@ fun MessageActionsBottomSheet( onDelete: () -> Unit, onDismiss: () -> Unit ) { + val context = LocalContext.current + val recentEmojiProvider = remember(context) { ReactionRecentEmojiProvider(context) } + var showEmojiPicker by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val sheetState = rememberModalBottomSheetState() + val backToActions: () -> Unit = { + showEmojiPicker = false + // Return to the actions list at its normal peek height, not fully expanded. + scope.launch { sheetState.partialExpand() } + } + ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surfaceContainerLow ) { - MessageActionsSheetContent( - actionsState = actionsState, - onEmojiClick = onEmojiClick, - onReply = onReply, - onReplyPrivately = onReplyPrivately, - onOpenThread = onOpenThread, - onForward = onForward, - onEdit = onEdit, - onCopy = onCopy, - onCopyMessageLink = onCopyMessageLink, - onMarkAsUnread = onMarkAsUnread, - onRemind = onRemind, - onPin = onPin, - onUnpin = onUnpin, - onTranslate = onTranslate, - onShareToNote = onShareToNote, - onShare = onShare, - onSave = onSave, - onOpenInFiles = onOpenInFiles, - onDelete = onDelete, - onDismiss = onDismiss - ) + if (showEmojiPicker) { + // The sheet's own Dialog window has its own back dispatcher, separate from the + // host Activity's - this BackHandler must be composed inside the sheet's content + // to intercept back before the sheet's onDismissRequest closes the whole thing. + BackHandler(onBack = backToActions) + EmojiPickerSheetContent( + recentEmojiProvider = recentEmojiProvider, + onEmojiSelected = { emoji -> + onEmojiClick(emoji) + onDismiss() + }, + onBack = backToActions + ) + } else { + MessageActionsSheetContent( + actionsState = actionsState, + recentEmojiProvider = recentEmojiProvider, + onEmojiClick = onEmojiClick, + onMoreEmojiClick = { showEmojiPicker = true }, + onReply = onReply, + onReplyPrivately = onReplyPrivately, + onOpenThread = onOpenThread, + onForward = onForward, + onEdit = onEdit, + onCopy = onCopy, + onCopyMessageLink = onCopyMessageLink, + onMarkAsUnread = onMarkAsUnread, + onRemind = onRemind, + onPin = onPin, + onUnpin = onUnpin, + onTranslate = onTranslate, + onShareToNote = onShareToNote, + onShare = onShare, + onSave = onSave, + onOpenInFiles = onOpenInFiles, + onDelete = onDelete, + onDismiss = onDismiss + ) + } } } @@ -331,7 +354,9 @@ fun MessageActionsBottomSheet( @Composable internal fun MessageActionsSheetContent( actionsState: MessageActionsState, + recentEmojiProvider: RecentEmojiProvider, onEmojiClick: (String) -> Unit, + onMoreEmojiClick: () -> Unit, onReply: () -> Unit, onReplyPrivately: () -> Unit, onOpenThread: () -> Unit, @@ -351,7 +376,6 @@ internal fun MessageActionsSheetContent( onDelete: () -> Unit, onDismiss: () -> Unit ) { - var actionsVisible by remember { mutableStateOf(true) } var showDeleteConfirmation by remember { mutableStateOf(false) } Column( @@ -362,195 +386,193 @@ internal fun MessageActionsSheetContent( if (actionsState.showEmojiBar) { EmojiBar( selfReactions = actionsState.selfReactions, + recentEmojiProvider = recentEmojiProvider, onEmojiClick = { emoji -> onEmojiClick(emoji) onDismiss() }, - onPickerShown = { actionsVisible = false }, - onPickerDismissed = { actionsVisible = true } + onMoreEmojiClick = onMoreEmojiClick ) } - if (actionsVisible) { - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - ) { - if (actionsState.showEditInfo) { - EditedInfo( - editedBy = actionsState.lastEditedBy, - editedAt = actionsState.lastEditedAt - ) - } - if (actionsState.showReply) { - MessageActionItem( - iconRes = R.drawable.ic_reply, - text = stringResource(R.string.nc_reply), - onClick = { - onReply() - onDismiss() - } - ) - } - if (actionsState.showReplyPrivately) { - MessageActionItem( - iconRes = R.drawable.ic_reply, - text = stringResource(R.string.nc_reply_privately), - onClick = { - onReplyPrivately() - onDismiss() - } - ) - } - if (actionsState.showOpenThread) { - MessageActionItem( - iconRes = R.drawable.outline_forum_24, - text = stringResource(R.string.open_thread), - onClick = { - onOpenThread() - onDismiss() - } - ) - } - if (actionsState.showForward) { - MessageActionItem( - iconRes = R.drawable.forward_24, - text = stringResource(R.string.nc_forward_message), - onClick = { - onForward() - onDismiss() - } - ) - } - if (actionsState.showEdit) { - MessageActionItem( - iconRes = R.drawable.ic_edit_24, - text = stringResource(R.string.nc_edit_message), - onClick = { - onEdit() - onDismiss() - } - ) - } - if (actionsState.showCopy) { - MessageActionItem( - iconRes = R.drawable.ic_content_copy, - text = stringResource(R.string.nc_copy_message), - onClick = { - onCopy() - onDismiss() - } - ) - } - if (actionsState.showCopyMessageLink) { - MessageActionItem( - iconRes = R.drawable.ic_open_in_new, - text = stringResource(R.string.nc_copy_message_link), - onClick = { - onCopyMessageLink() - onDismiss() - } - ) - } - if (actionsState.showMarkAsUnread) { - MessageActionItem( - iconRes = R.drawable.ic_mark_chat_unread_24px, - text = stringResource(R.string.nc_mark_as_unread), - onClick = { - onMarkAsUnread() - onDismiss() - } - ) - } - if (actionsState.showRemind) { - MessageActionItem( - iconRes = R.drawable.ic_timer_black_24dp, - text = stringResource(R.string.nc_remind), - onClick = { - onRemind() - onDismiss() - } - ) - } - if (actionsState.showPin) { - if (actionsState.isPinned) { - MessageActionItem( - iconRes = R.drawable.keep_off_24px, - text = stringResource(R.string.unpin_message), - onClick = { - onUnpin() - onDismiss() - } - ) - } else { - MessageActionItem( - iconRes = R.drawable.keep_24px, - text = stringResource(R.string.pin_message), - onClick = { - onPin() - onDismiss() - } - ) + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + if (actionsState.showEditInfo) { + EditedInfo( + editedBy = actionsState.lastEditedBy, + editedAt = actionsState.lastEditedAt + ) + } + if (actionsState.showReply) { + MessageActionItem( + iconRes = R.drawable.ic_reply, + text = stringResource(R.string.nc_reply), + onClick = { + onReply() + onDismiss() } - } - if (actionsState.showTranslate) { - MessageActionItem( - iconRes = R.drawable.ic_baseline_translate_24, - text = stringResource(R.string.translate), - onClick = { - onTranslate() - onDismiss() - } - ) - } - if (actionsState.showShareToNote) { - MessageActionItem( - iconRes = R.drawable.ic_edit_note_24, - text = stringResource(R.string.add_to_notes), - onClick = { - onShareToNote() - onDismiss() - } - ) - } - if (actionsState.showShare) { - MessageActionItem( - iconRes = R.drawable.ic_share_action, - text = stringResource(R.string.share), - onClick = { - onShare() - onDismiss() - } - ) - } - if (actionsState.showSave) { + ) + } + if (actionsState.showReplyPrivately) { + MessageActionItem( + iconRes = R.drawable.ic_reply, + text = stringResource(R.string.nc_reply_privately), + onClick = { + onReplyPrivately() + onDismiss() + } + ) + } + if (actionsState.showOpenThread) { + MessageActionItem( + iconRes = R.drawable.outline_forum_24, + text = stringResource(R.string.open_thread), + onClick = { + onOpenThread() + onDismiss() + } + ) + } + if (actionsState.showForward) { + MessageActionItem( + iconRes = R.drawable.forward_24, + text = stringResource(R.string.nc_forward_message), + onClick = { + onForward() + onDismiss() + } + ) + } + if (actionsState.showEdit) { + MessageActionItem( + iconRes = R.drawable.ic_edit_24, + text = stringResource(R.string.nc_edit_message), + onClick = { + onEdit() + onDismiss() + } + ) + } + if (actionsState.showCopy) { + MessageActionItem( + iconRes = R.drawable.ic_content_copy, + text = stringResource(R.string.nc_copy_message), + onClick = { + onCopy() + onDismiss() + } + ) + } + if (actionsState.showCopyMessageLink) { + MessageActionItem( + iconRes = R.drawable.ic_open_in_new, + text = stringResource(R.string.nc_copy_message_link), + onClick = { + onCopyMessageLink() + onDismiss() + } + ) + } + if (actionsState.showMarkAsUnread) { + MessageActionItem( + iconRes = R.drawable.ic_mark_chat_unread_24px, + text = stringResource(R.string.nc_mark_as_unread), + onClick = { + onMarkAsUnread() + onDismiss() + } + ) + } + if (actionsState.showRemind) { + MessageActionItem( + iconRes = R.drawable.ic_timer_black_24dp, + text = stringResource(R.string.nc_remind), + onClick = { + onRemind() + onDismiss() + } + ) + } + if (actionsState.showPin) { + if (actionsState.isPinned) { MessageActionItem( - iconRes = R.drawable.baseline_download_24, - text = stringResource(R.string.nc_save_message), + iconRes = R.drawable.keep_off_24px, + text = stringResource(R.string.unpin_message), onClick = { - onSave() + onUnpin() onDismiss() } ) - } - if (actionsState.showOpenInFiles) { + } else { MessageActionItem( - iconRes = R.drawable.ic_exit_to_app_black_24dp, - text = stringResource(R.string.open_in_files_app), + iconRes = R.drawable.keep_24px, + text = stringResource(R.string.pin_message), onClick = { - onOpenInFiles() + onPin() onDismiss() } ) } - if (actionsState.showDelete) { - MessageActionItem( - iconRes = R.drawable.ic_delete, - text = stringResource(R.string.nc_delete), - onClick = { showDeleteConfirmation = true } - ) - } + } + if (actionsState.showTranslate) { + MessageActionItem( + iconRes = R.drawable.ic_baseline_translate_24, + text = stringResource(R.string.translate), + onClick = { + onTranslate() + onDismiss() + } + ) + } + if (actionsState.showShareToNote) { + MessageActionItem( + iconRes = R.drawable.ic_edit_note_24, + text = stringResource(R.string.add_to_notes), + onClick = { + onShareToNote() + onDismiss() + } + ) + } + if (actionsState.showShare) { + MessageActionItem( + iconRes = R.drawable.ic_share_action, + text = stringResource(R.string.share), + onClick = { + onShare() + onDismiss() + } + ) + } + if (actionsState.showSave) { + MessageActionItem( + iconRes = R.drawable.baseline_download_24, + text = stringResource(R.string.nc_save_message), + onClick = { + onSave() + onDismiss() + } + ) + } + if (actionsState.showOpenInFiles) { + MessageActionItem( + iconRes = R.drawable.ic_exit_to_app_black_24dp, + text = stringResource(R.string.open_in_files_app), + onClick = { + onOpenInFiles() + onDismiss() + } + ) + } + if (actionsState.showDelete) { + MessageActionItem( + iconRes = R.drawable.ic_delete, + text = stringResource(R.string.nc_delete), + onClick = { showDeleteConfirmation = true } + ) } } } @@ -569,13 +591,16 @@ internal fun MessageActionsSheetContent( @Composable internal fun EmojiBar( selfReactions: Set, + recentEmojiProvider: RecentEmojiProvider, onEmojiClick: (String) -> Unit, - onPickerShown: () -> Unit, - onPickerDismissed: () -> Unit + onMoreEmojiClick: () -> Unit ) { - val context = LocalContext.current - val recentEmojiManager = remember(context) { RecentEmojiManager(context, MAX_RECENTS) } - val emojis: List = remember(context) { buildEmojiList(recentEmojiManager) } + var emojis by remember { mutableStateOf(defaultReactionEmojis) } + + LaunchedEffect(recentEmojiProvider) { + val recents = recentEmojiProvider.getRecentEmojiList() + emojis = (recents + defaultReactionEmojis).distinct().take(MAX_RECENTS) + } val startPadding = dimensionResource(R.dimen.standard_padding) val emojiButtonSize = dimensionResource(R.dimen.reaction_bottom_sheet_layout_size) @@ -602,20 +627,11 @@ internal fun EmojiBar( isSelected = selfReactions.contains(unicodeEmoji), onClick = { onEmojiClick(unicodeEmoji) - val keyword = emojiSearchKeywords[unicodeEmoji] ?: "" - val results = SearchEmojiManager().search(keyword) - if (results.isNotEmpty()) { - recentEmojiManager.addEmoji(results[0].component1()) - recentEmojiManager.persist() - } + recentEmojiProvider.recordSelection(unicodeEmoji) } ) } - MoreEmojiButton( - onEmojiSelected = onEmojiClick, - onPickerShown = onPickerShown, - onPickerDismissed = onPickerDismissed - ) + MoreEmojiButton(onClick = onMoreEmojiClick) } } } @@ -639,78 +655,61 @@ private fun EmojiButton(emoji: String, isSelected: Boolean, onClick: () -> Unit) } } -@SuppressLint("ClickableViewAccessibility") -@Suppress("LongMethod") @Composable -private fun MoreEmojiButton( +private fun MoreEmojiButton(onClick: () -> Unit) { + Surface( + onClick = onClick, + shape = CircleShape, + color = Color.Transparent, + modifier = Modifier + .width(dimensionResource(R.dimen.activity_row_layout_height)) + .height(dimensionResource(R.dimen.activity_row_layout_height)) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(R.drawable.ic_dots_horizontal), + contentDescription = stringResource(R.string.emoji_more) + ) + } + } +} + +@Composable +private fun EmojiPickerSheetContent( + recentEmojiProvider: RecentEmojiProvider, onEmojiSelected: (String) -> Unit, - onPickerShown: () -> Unit, - onPickerDismissed: () -> Unit + onBack: () -> Unit ) { - val context = LocalContext.current - val rootView = LocalView.current - val theming = emojiTheming() - val popupRef = remember { mutableStateOf(null) } + val backgroundColor = MaterialTheme.colorScheme.surfaceContainerLow + val selectedTabColor = MaterialTheme.colorScheme.primary.toArgb() + val unselectedTabColor = MaterialTheme.colorScheme.onSurfaceVariant.toArgb() - DisposableEffect(Unit) { - onDispose { popupRef.value?.dismiss() } - } - - AndroidView( - factory = { ctx -> - EmojiEditText(ctx).apply { - setBackgroundColor(android.graphics.Color.TRANSPARENT) - val drawable = ContextCompat.getDrawable(ctx, R.drawable.ic_dots_horizontal) - setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null) - contentDescription = ctx.getString(R.string.emoji_more) - - val emojiPopup = EmojiPopup( - rootView = rootView, - editText = this, - theming = theming, - onEmojiPopupShownListener = { - clearFocus() - onPickerShown() - }, - onEmojiClickListener = { emoji -> - popupRef.value?.dismiss() - onEmojiSelected(emoji.unicode) - }, - onEmojiPopupDismissListener = { - clearFocus() - val imm = ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(windowToken, 0) - onPickerDismissed() - } - ) - installDisableKeyboardInput(emojiPopup) - installForceSingleEmoji() - setOnTouchListener { _, event -> - if (event.action == MotionEvent.ACTION_DOWN) { - val popup = emojiPopup - if (popup.isShowing) { - popup.dismiss() - } else { - popup.show() - // workaround for first-open bug (see issue #1914) - Handler(Looper.getMainLooper()).postDelayed( - { - popup.dismiss() - popup.show() - }, - EMOJI_POPUP_TOGGLE_DELAY - ) - } - } - true + Column( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + ) { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_button)) + } + AndroidView( + modifier = Modifier + .fillMaxWidth() + .height(emojiPickerHeight), + factory = { ctx -> + EmojiPickerView(ContextThemeWrapper(ctx, R.style.ThemeOverlay_App_EmojiPicker)).apply { + setBackgroundColor(backgroundColor.toArgb()) + setRecentEmojiProvider(recentEmojiProvider) + setOnEmojiPickedListener(Consumer { item -> onEmojiSelected(item.emoji) }) + themeEmojiPickerCategoryTabs(this, selectedTabColor, unselectedTabColor) } - popupRef.value = emojiPopup } - }, - modifier = Modifier - .width(dimensionResource(R.dimen.activity_row_layout_height)) - .height(dimensionResource(R.dimen.activity_row_layout_height)) - ) + ) + } } @Composable @@ -801,9 +800,9 @@ private fun PreviewEmojiBar() { Surface(color = MaterialTheme.colorScheme.surfaceContainerLow) { EmojiBar( selfReactions = setOf("👍", "❤️"), + recentEmojiProvider = ReactionRecentEmojiProvider(LocalContext.current), onEmojiClick = {}, - onPickerShown = {}, - onPickerDismissed = {} + onMoreEmojiClick = {} ) } } @@ -843,7 +842,9 @@ private fun PreviewMessageActionsSheetContent() { Surface(color = MaterialTheme.colorScheme.surfaceContainerLow) { MessageActionsSheetContent( actionsState = previewState, + recentEmojiProvider = ReactionRecentEmojiProvider(LocalContext.current), onEmojiClick = {}, + onMoreEmojiClick = {}, onReply = {}, onReplyPrivately = {}, onOpenThread = {}, @@ -897,7 +898,9 @@ private fun PreviewMessageActionsSheetPinned() { showOpenInFiles = false, showDelete = false ), + recentEmojiProvider = ReactionRecentEmojiProvider(LocalContext.current), onEmojiClick = {}, + onMoreEmojiClick = {}, onReply = {}, onReplyPrivately = {}, onOpenThread = {}, diff --git a/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageInputComponents.kt b/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageInputComponents.kt index 3b7e69f0ab1..18fa6f7d467 100644 --- a/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageInputComponents.kt +++ b/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageInputComponents.kt @@ -7,10 +7,9 @@ package com.nextcloud.talk.chooseaccount.ui -import android.view.Gravity -import android.view.inputmethod.InputMethodManager import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -42,32 +41,23 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.getSystemService import com.nextcloud.talk.R -import com.nextcloud.talk.ui.theme.emojiTheming -import com.vanniktech.emoji.EmojiEditText -import com.vanniktech.emoji.EmojiPopup -import com.vanniktech.emoji.installDisableKeyboardInput -import com.vanniktech.emoji.installForceSingleEmoji @Composable internal fun EmojiAndMessageRow( emoji: String, message: String, - onEmojiSelected: (String) -> Unit, + onEmojiButtonClick: () -> Unit, onMessageChanged: (String) -> Unit ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { - EmojiButton(emoji = emoji, onEmojiSelected = onEmojiSelected) + EmojiButton(emoji = emoji, onClick = onEmojiButtonClick) Spacer(modifier = Modifier.width(8.dp)) OutlinedTextField( value = message, @@ -81,11 +71,7 @@ internal fun EmojiAndMessageRow( } @Composable -private fun EmojiButton(emoji: String, onEmojiSelected: (String) -> Unit) { - val isPreview = LocalInspectionMode.current - val rootView = LocalView.current - val theming = emojiTheming() - var emojiPopup by remember { mutableStateOf(null) } +private fun EmojiButton(emoji: String, onClick: () -> Unit) { val displayEmoji = emoji.ifEmpty { stringResource(R.string.default_emoji) } Card( @@ -94,48 +80,12 @@ private fun EmojiButton(emoji: String, onEmojiSelected: (String) -> Unit) { elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), modifier = Modifier .size(56.dp) - .clickable { emojiPopup?.show() } + .clickable(onClick = onClick) ) { - if (isPreview) { + Box(modifier = Modifier.size(56.dp), contentAlignment = Alignment.Center) { Text( text = displayEmoji, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier - .size(56.dp) - .padding(14.dp) - ) - } else { - AndroidView( - modifier = Modifier.size(56.dp), - factory = { ctx -> - EmojiEditText(ctx).apply { - setText(displayEmoji) - gravity = Gravity.CENTER - textSize = EMOJI_TEXT_SIZE_SP - background = null - isCursorVisible = false - val popup = EmojiPopup( - rootView = rootView, - editText = this, - theming = theming, - onEmojiClickListener = { - onEmojiSelected(text.toString()) - emojiPopup?.dismiss() - clearFocus() - ctx.getSystemService() - ?.hideSoftInputFromWindow(windowToken, 0) - } - ) - installDisableKeyboardInput(popup) - installForceSingleEmoji() - emojiPopup = popup - } - }, - update = { view -> - if (view.text.toString() != displayEmoji) { - view.setText(displayEmoji) - } - } + style = MaterialTheme.typography.titleLarge ) } } @@ -223,5 +173,3 @@ internal fun ActionButtons(onClear: () -> Unit, onSet: () -> Unit) { } } } - -private const val EMOJI_TEXT_SIZE_SP = 24f diff --git a/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheet.kt b/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheet.kt index c57ae6d7cd7..fee0a1301a0 100644 --- a/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheet.kt +++ b/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheet.kt @@ -8,14 +8,21 @@ package com.nextcloud.talk.chooseaccount.ui import android.content.res.Configuration +import android.view.ContextThemeWrapper +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text @@ -24,21 +31,33 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.util.Consumer +import androidx.emoji2.emojipicker.EmojiPickerView import com.nextcloud.talk.R import com.nextcloud.talk.chooseaccount.viewmodel.StatusMessageViewModel import com.nextcloud.talk.models.json.status.Status import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus +import com.nextcloud.talk.ui.theme.themeEmojiPickerCategoryTabs + +private val emojiPickerHeight = 360.dp @OptIn(ExperimentalMaterial3Api::class) @Composable fun StatusMessageModalBottomSheet(currentStatus: Status, viewModel: StatusMessageViewModel, onDismiss: () -> Unit) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val isDismissed by viewModel.isDismissed.collectAsState() + var showEmojiPicker by remember { mutableStateOf(false) } + val backToStatus: () -> Unit = { showEmojiPicker = false } LaunchedEffect(currentStatus) { viewModel.init(currentStatus) @@ -54,12 +73,59 @@ fun StatusMessageModalBottomSheet(currentStatus: Status, viewModel: StatusMessag onDismissRequest = onDismiss, sheetState = sheetState ) { - StatusMessageSheetContent(viewModel = viewModel) + if (showEmojiPicker) { + // The sheet's own Dialog window has its own back dispatcher, separate from the + // host Activity's - this BackHandler must be composed inside the sheet's content + // to intercept back before the sheet's onDismissRequest closes the whole thing. + BackHandler(onBack = backToStatus) + EmojiPickerSheetContent( + onEmojiSelected = { emoji -> + viewModel.updateEmoji(emoji) + backToStatus() + }, + onBack = backToStatus + ) + } else { + StatusMessageSheetContent(viewModel = viewModel, onEmojiButtonClick = { showEmojiPicker = true }) + } } } @Composable -fun StatusMessageSheetContent(modifier: Modifier = Modifier, viewModel: StatusMessageViewModel) { +private fun EmojiPickerSheetContent(onEmojiSelected: (String) -> Unit, onBack: () -> Unit) { + val backgroundColor = MaterialTheme.colorScheme.surfaceContainerLow + val selectedTabColor = MaterialTheme.colorScheme.primary.toArgb() + val unselectedTabColor = MaterialTheme.colorScheme.onSurfaceVariant.toArgb() + + Column( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + ) { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_button)) + } + AndroidView( + modifier = Modifier + .fillMaxWidth() + .height(emojiPickerHeight), + factory = { ctx -> + EmojiPickerView(ContextThemeWrapper(ctx, R.style.ThemeOverlay_App_EmojiPicker)).apply { + setBackgroundColor(backgroundColor.toArgb()) + setOnEmojiPickedListener(Consumer { item -> onEmojiSelected(item.emoji) }) + themeEmojiPickerCategoryTabs(this, selectedTabColor, unselectedTabColor) + } + } + ) + } +} + +@Composable +fun StatusMessageSheetContent( + modifier: Modifier = Modifier, + viewModel: StatusMessageViewModel, + onEmojiButtonClick: () -> Unit +) { val emoji by viewModel.emoji.collectAsState() val message by viewModel.message.collectAsState() val clearAtPosition by viewModel.clearAtPosition.collectAsState() @@ -73,7 +139,7 @@ fun StatusMessageSheetContent(modifier: Modifier = Modifier, viewModel: StatusMe clearAtPosition = clearAtPosition, predefinedStatuses = predefinedStatuses, isBackupStatusAvailable = isBackupStatusAvailable, - onEmojiSelected = { viewModel.updateEmoji(it) }, + onEmojiButtonClick = onEmojiButtonClick, onMessageChanged = { viewModel.updateMessage(it) }, onClearAtPositionSelected = { viewModel.updateClearAtPosition(it) }, onRevertStatus = { viewModel.revertStatus() }, @@ -92,7 +158,7 @@ internal fun StatusMessageSheetContentStateless( clearAtPosition: Int, predefinedStatuses: List, isBackupStatusAvailable: Boolean, - onEmojiSelected: (String) -> Unit, + onEmojiButtonClick: () -> Unit, onMessageChanged: (String) -> Unit, onClearAtPositionSelected: (Int) -> Unit, onRevertStatus: () -> Unit, @@ -109,7 +175,7 @@ internal fun StatusMessageSheetContentStateless( clearAtPosition = clearAtPosition, predefinedStatuses = predefinedStatuses, isBackupStatusAvailable = isBackupStatusAvailable, - onEmojiSelected = onEmojiSelected, + onEmojiButtonClick = onEmojiButtonClick, onMessageChanged = onMessageChanged, onClearAtPositionSelected = onClearAtPositionSelected, onRevertStatus = onRevertStatus, @@ -125,7 +191,7 @@ internal fun StatusMessageSheetContentStateless( clearAtPosition = clearAtPosition, predefinedStatuses = predefinedStatuses, isBackupStatusAvailable = isBackupStatusAvailable, - onEmojiSelected = onEmojiSelected, + onEmojiButtonClick = onEmojiButtonClick, onMessageChanged = onMessageChanged, onClearAtPositionSelected = onClearAtPositionSelected, onRevertStatus = onRevertStatus, @@ -145,7 +211,7 @@ private fun LandscapeSheetContent( clearAtPosition: Int, predefinedStatuses: List, isBackupStatusAvailable: Boolean, - onEmojiSelected: (String) -> Unit, + onEmojiButtonClick: () -> Unit, onMessageChanged: (String) -> Unit, onClearAtPositionSelected: (Int) -> Unit, onRevertStatus: () -> Unit, @@ -164,7 +230,7 @@ private fun LandscapeSheetContent( EmojiAndMessageRow( emoji = emoji, message = message, - onEmojiSelected = onEmojiSelected, + onEmojiButtonClick = onEmojiButtonClick, onMessageChanged = onMessageChanged ) Spacer(modifier = Modifier.height(12.dp)) @@ -191,7 +257,7 @@ private fun PortraitSheetContent( clearAtPosition: Int, predefinedStatuses: List, isBackupStatusAvailable: Boolean, - onEmojiSelected: (String) -> Unit, + onEmojiButtonClick: () -> Unit, onMessageChanged: (String) -> Unit, onClearAtPositionSelected: (Int) -> Unit, onRevertStatus: () -> Unit, @@ -208,7 +274,7 @@ private fun PortraitSheetContent( EmojiAndMessageRow( emoji = emoji, message = message, - onEmojiSelected = onEmojiSelected, + onEmojiButtonClick = onEmojiButtonClick, onMessageChanged = onMessageChanged ) PredefinedStatusList( diff --git a/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheetPreview.kt b/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheetPreview.kt index 1d060ee8853..2f390c9f6e1 100644 --- a/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheetPreview.kt +++ b/app/src/main/java/com/nextcloud/talk/chooseaccount/ui/StatusMessageSheetPreview.kt @@ -46,7 +46,7 @@ private fun PreviewStatusMessageSheet() { clearAtPosition = 3, predefinedStatuses = previewPredefinedStatuses(), isBackupStatusAvailable = false, - onEmojiSelected = {}, + onEmojiButtonClick = {}, onMessageChanged = {}, onClearAtPositionSelected = {}, onRevertStatus = {}, @@ -78,7 +78,7 @@ private fun PreviewStatusMessageSheetRtl() { clearAtPosition = 1, predefinedStatuses = previewPredefinedStatuses(), isBackupStatusAvailable = false, - onEmojiSelected = {}, + onEmojiButtonClick = {}, onMessageChanged = {}, onClearAtPositionSelected = {}, onRevertStatus = {}, @@ -123,7 +123,7 @@ private fun PreviewStatusMessageSheetWithBackup() { clearAtPosition = 0, predefinedStatuses = previewPredefinedStatusesWithBackup(), isBackupStatusAvailable = true, - onEmojiSelected = {}, + onEmojiButtonClick = {}, onMessageChanged = {}, onClearAtPositionSelected = {}, onRevertStatus = {}, diff --git a/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt index 229c745055c..94adbd650d3 100644 --- a/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt @@ -8,7 +8,6 @@ package com.nextcloud.talk.conversation import android.annotation.SuppressLint import android.app.Dialog -import android.content.res.ColorStateList import android.os.Bundle import android.text.Editable import android.text.TextUtils @@ -18,7 +17,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog -import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.DialogFragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider @@ -27,7 +25,6 @@ import androidx.lifecycle.repeatOnLifecycle import autodagger.AutoInjector import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar -import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel @@ -35,7 +32,6 @@ import com.nextcloud.talk.conversationlist.ConversationsListActivity import com.nextcloud.talk.databinding.DialogRenameConversationBinding import com.nextcloud.talk.events.ConversationsListFetchDataEvent import com.nextcloud.talk.ui.theme.ViewThemeUtils -import com.vanniktech.emoji.EmojiPopup import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import javax.inject.Inject @@ -55,8 +51,6 @@ class RenameConversationDialogFragment : DialogFragment() { private lateinit var binding: DialogRenameConversationBinding private lateinit var viewModel: ConversationInfoEditViewModel - private var emojiPopup: EmojiPopup? = null - private var roomToken = "" private var initialName = "" @@ -94,8 +88,6 @@ class RenameConversationDialogFragment : DialogFragment() { setupListeners() setupStateObserver() - - setupEmojiPopup() } override fun onStart() { @@ -118,33 +110,7 @@ class RenameConversationDialogFragment : DialogFragment() { viewThemeUtils.material.colorTextInputLayout(binding.textInputLayout) } - private fun setupEmojiPopup() { - emojiPopup = binding.let { - EmojiPopup( - rootView = requireView(), - editText = it.textEdit, - theming = viewThemeUtils.talk.getEmojiTheming(requireContext()), - onEmojiPopupShownListener = { - viewThemeUtils.platform.colorImageView(it.smileyButton, ColorRole.PRIMARY) - }, - onEmojiPopupDismissListener = { - it.smileyButton.imageTintList = ColorStateList.valueOf( - ResourcesCompat.getColor( - resources, - R.color.medium_emphasis_text, - context?.theme - ) - ) - }, - onEmojiClickListener = { - binding.textEdit.editableText?.append(" ") - } - ) - } - } - private fun setupListeners() { - binding.smileyButton.setOnClickListener { emojiPopup?.toggle() } binding.textEdit.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { // unused atm 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 eaad8c81d0c..5c30b385284 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt @@ -26,7 +26,6 @@ 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 @@ -71,7 +70,6 @@ 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/dialog/MoreCallActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt index 6653c4278b9..bfc10a712be 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt @@ -12,6 +12,7 @@ import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import androidx.core.content.ContextCompat +import androidx.emoji2.widget.EmojiTextView import autodagger.AutoInjector import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog @@ -26,7 +27,6 @@ import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.CapabilitiesUtil import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.viewmodels.CallRecordingViewModel -import com.vanniktech.emoji.EmojiTextView import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/EmojiPickerCategoryTabs.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/EmojiPickerCategoryTabs.kt new file mode 100644 index 00000000000..d5d0d29ad15 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/theme/EmojiPickerCategoryTabs.kt @@ -0,0 +1,87 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.theme + +import android.content.res.ColorStateList +import android.view.View +import android.view.ViewTreeObserver +import android.widget.ImageView +import androidx.emoji2.emojipicker.EmojiPickerView +import androidx.recyclerview.widget.RecyclerView +import androidx.emoji2.emojipicker.R as EmojiPickerR + +/** + * androidx.emoji2.emojipicker.EmojiPickerView tints its selected category tab via the + * platform `?android:attr/colorAccent`, which this app never wires up to its dynamic + * per-server branding (that color only exists as a runtime value computed from + * [com.nextcloud.android.common.ui.theme.MaterialSchemes], not as a theme attribute). + * This reaches into the picker's header RecyclerView - not public API, but stable + * since the library's initial release - to retint each category tab directly. + * + * EmojiPickerView builds its header/body views asynchronously: it loads the bundled + * emoji data on a background coroutine and only inflates its real layout afterwards, so + * the header doesn't exist yet right after construction. This waits for that via a + * global layout listener, then reapplies tint on every adapter rebind too, since + * EmojiPickerHeaderAdapter.selectedGroupIndex rebinds the old and new tab on every + * category change. Safe no-op if the internal view IDs ever change in a future version. + */ +fun themeEmojiPickerCategoryTabs(emojiPickerView: EmojiPickerView, selectedColor: Int, unselectedColor: Int) { + val tint = ColorStateList( + arrayOf(intArrayOf(android.R.attr.state_selected), intArrayOf()), + intArrayOf(selectedColor, unselectedColor) + ) + + fun tintTab(tabView: View) { + tabView.findViewById(EmojiPickerR.id.emoji_picker_header_icon)?.imageTintList = tint + tabView.findViewById(EmojiPickerR.id.emoji_picker_header_underline)?.backgroundTintList = tint + } + + fun tintAllTabs(header: RecyclerView) { + for (i in 0 until header.childCount) { + tintTab(header.getChildAt(i)) + } + } + + fun attachTo(header: RecyclerView) { + tintAllTabs(header) + + header.addOnChildAttachStateChangeListener( + object : RecyclerView.OnChildAttachStateChangeListener { + override fun onChildViewAttachedToWindow(view: View) = tintTab(view) + override fun onChildViewDetachedFromWindow(view: View) = Unit + } + ) + + header.adapter?.registerAdapterDataObserver( + object : RecyclerView.AdapterDataObserver() { + override fun onChanged() = reapply() + override fun onItemRangeChanged(positionStart: Int, itemCount: Int) = reapply() + + private fun reapply() { + header.post { tintAllTabs(header) } + } + } + ) + } + + val header = emojiPickerView.findViewById(EmojiPickerR.id.emoji_picker_header) + if (header != null) { + attachTo(header) + return + } + + emojiPickerView.viewTreeObserver.addOnGlobalLayoutListener( + object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + val lateHeader = + emojiPickerView.findViewById(EmojiPickerR.id.emoji_picker_header) ?: return + emojiPickerView.viewTreeObserver.removeOnGlobalLayoutListener(this) + attachTo(lateHeader) + } + } + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/EmojiThemings.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/EmojiThemings.kt deleted file mode 100644 index f5bcbd69816..00000000000 --- a/app/src/main/java/com/nextcloud/talk/ui/theme/EmojiThemings.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package com.nextcloud.talk.ui.theme - -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.graphics.toArgb -import com.vanniktech.emoji.EmojiTheming - -/** - * Themes the vanniktech emoji popup from the current [MaterialTheme.colorScheme] so its category, - * search and backspace icons stay visible in dark mode. - * Keep in sync with the view variant [TalkSpecificViewThemeUtils.getEmojiTheming]. - */ -@Composable -@ReadOnlyComposable -fun emojiTheming(): EmojiTheming = - with(MaterialTheme.colorScheme) { - EmojiTheming( - backgroundColor = surface.toArgb(), - primaryColor = onSurfaceVariant.toArgb(), - secondaryColor = primary.toArgb(), - dividerColor = outlineVariant.toArgb(), - textColor = onSurface.toArgb(), - textSecondaryColor = onSurfaceVariant.toArgb() - ) - } 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 4bce1f1a71c..78fd8801fbf 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 @@ -52,8 +52,6 @@ import com.nextcloud.talk.ui.WaveformSeekBar import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DrawableUtils import com.nextcloud.talk.utils.message.MessageUtils -import com.vanniktech.emoji.EmojiTextView -import com.vanniktech.emoji.EmojiTheming import dynamiccolor.DynamicScheme import dynamiccolor.MaterialDynamicColors import eu.davidea.flexibleadapter.utils.FlexibleUtils @@ -170,35 +168,6 @@ class TalkSpecificViewThemeUtils @Inject constructor( appcompat.colorSwitchCompat(switch) } - fun setCheckedBackground(emoji: EmojiTextView) { - withScheme(emoji) { scheme -> - val drawable = AppCompatResources - .getDrawable(emoji.context, R.drawable.reaction_self_bottom_sheet_background)!! - .mutate() - DrawableCompat.setTintList( - drawable, - ColorStateList.valueOf(dynamicColor.primary().getArgb(scheme)) - ) - emoji.background = drawable - } - } - - /** - * Themes the vanniktech emoji popup so its category, search and backspace icons stay visible in dark mode. - * Keep in sync with the Compose variant [emojiTheming]. - */ - fun getEmojiTheming(context: Context): EmojiTheming = - withScheme(context) { scheme -> - EmojiTheming( - backgroundColor = dynamicColor.surface().getArgb(scheme), - primaryColor = dynamicColor.onSurfaceVariant().getArgb(scheme), - secondaryColor = dynamicColor.primary().getArgb(scheme), - dividerColor = dynamicColor.outlineVariant().getArgb(scheme), - textColor = dynamicColor.onSurface().getArgb(scheme), - textSecondaryColor = dynamicColor.onSurfaceVariant().getArgb(scheme) - ) - } - fun setReactionsBackground(linearLayout: LinearLayout, outgoing: Boolean, isBubbled: Boolean) { withScheme(linearLayout) { scheme -> val drawable = AppCompatResources diff --git a/app/src/main/java/com/nextcloud/talk/utils/TextMatchers.java b/app/src/main/java/com/nextcloud/talk/utils/TextMatchers.java index 88906a1ecf3..5a047cad33a 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/TextMatchers.java +++ b/app/src/main/java/com/nextcloud/talk/utils/TextMatchers.java @@ -9,19 +9,38 @@ */ package com.nextcloud.talk.utils; -import com.vanniktech.emoji.EmojiInformation; -import com.vanniktech.emoji.Emojis; +import android.text.Spanned; import androidx.annotation.Nullable; +import androidx.emoji2.text.EmojiCompat; +import androidx.emoji2.text.EmojiSpan; public final class TextMatchers { public static boolean isMessageWithSingleEmoticonOnly(@Nullable final String text) { - if (text != null) { - final EmojiInformation emojiInformation = Emojis.emojiInformation(text); - return (emojiInformation.isOnlyEmojis && emojiInformation.getEmojiCount() == 1); - } else { + if (text == null) { return false; } + + final String trimmed = text.trim(); + if (trimmed.isEmpty()) { + return false; + } + + final EmojiCompat emojiCompat = EmojiCompat.get(); + if (emojiCompat.getLoadState() != EmojiCompat.LOAD_STATE_SUCCEEDED) { + return false; + } + + final CharSequence processed = emojiCompat.process(trimmed); + if (!(processed instanceof Spanned)) { + return false; + } + + final Spanned spanned = (Spanned) processed; + final EmojiSpan[] spans = spanned.getSpans(0, spanned.length(), EmojiSpan.class); + return spans.length == 1 + && spanned.getSpanStart(spans[0]) == 0 + && spanned.getSpanEnd(spans[0]) == trimmed.length(); } } diff --git a/app/src/main/res/layout/dialog_rename_conversation.xml b/app/src/main/res/layout/dialog_rename_conversation.xml index 734ba9171f2..400c7c52f41 100644 --- a/app/src/main/res/layout/dialog_rename_conversation.xml +++ b/app/src/main/res/layout/dialog_rename_conversation.xml @@ -20,8 +20,6 @@ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/standard_half_margin" - android:layout_toStartOf="@id/smileyButton" android:hint="@string/nc_call_name" app:errorTextAppearance="@style/ErrorAppearance" app:passwordToggleTint="@color/grey_600" @@ -40,17 +38,4 @@ - - diff --git a/app/src/main/res/layout/rv_item_conversation_info_participant.xml b/app/src/main/res/layout/rv_item_conversation_info_participant.xml index 94520135ecb..309c216412f 100644 --- a/app/src/main/res/layout/rv_item_conversation_info_participant.xml +++ b/app/src/main/res/layout/rv_item_conversation_info_participant.xml @@ -22,12 +22,11 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - - + android:layout_toStartOf="@id/messageSendButton" /> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 408e6ea7bbb..95b5850525b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -48,6 +48,17 @@ true + + +