From 2ca015a0aedd818f87dda832297baf148dbfb02a Mon Sep 17 00:00:00 2001 From: octoshrimpy Date: Sun, 14 Jun 2026 18:58:13 -0400 Subject: [PATCH] feat(reactions): grid emoji picker, configurable quick reactions, open animation - Fix swipe-to-react: enable stable ids on MessagesAdapter so the swipe callback resolves the swiped message (itemId was NO_ID -> lookup failed), and anchor the reaction popup independent of swipe-recovery translation so it no longer opens off-screen. - Replace the typed-text reaction "+" with a themed, scrollable emoji grid (EmojiPicker) generated from colour-emoji codepoint ranges plus a curated common set. No third-party picker, so no Guava and it follows light/dark. - Make the six quick reactions user-configurable and reorderable via a new Settings -> Quick reactions editor; the double-tap default reaction now uses the same grid picker instead of a raw text dialog. - Pulse visible reaction badges (1 -> 1.4 -> 0.9 -> 1, cascaded 0.25s) when a conversation opens with a reaction as its newest message. Co-Authored-By: Claude Opus 4.8 --- .../java/com/moez/QKSMS/util/Preferences.kt | 6 + .../com/moez/QKSMS/common/util/EmojiPicker.kt | 119 ++++++++++++++++++ .../QKSMS/feature/compose/ComposeActivity.kt | 44 ++++--- .../QKSMS/feature/compose/MessagesAdapter.kt | 66 ++++++++++ .../feature/settings/SettingsController.kt | 102 ++++++++++++++- .../feature/settings/SettingsPresenter.kt | 11 ++ .../QKSMS/feature/settings/SettingsState.kt | 1 + .../QKSMS/feature/settings/SettingsView.kt | 2 + .../main/res/layout/settings_controller.xml | 8 ++ presentation/src/main/res/values/strings.xml | 3 + 10 files changed, 338 insertions(+), 24 deletions(-) create mode 100644 presentation/src/main/java/com/moez/QKSMS/common/util/EmojiPicker.kt diff --git a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt index a44972e8..2a144c11 100644 --- a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -93,6 +93,10 @@ class Preferences @Inject constructor( const val MESSAGE_SWIPE_ACTION_DELETE = 5 const val DEFAULT_REACTION_EMOJI = "👍" + + // Quick-reaction emojis shown in the reaction popup, comma separated (user reorderable) + const val DEFAULT_QUICK_REACTIONS = "❤️,👍,👎,😂,😮,😢" + const val QUICK_REACTIONS_COUNT = 6 } // Internal @@ -133,6 +137,8 @@ class Preferences @Inject constructor( val swipeRight = rxPrefs.getInteger("swipeRight", SWIPE_ACTION_ARCHIVE) val swipeLeft = rxPrefs.getInteger("swipeLeft", SWIPE_ACTION_ARCHIVE) val defaultReactionEmoji = rxPrefs.getString("defaultReactionEmoji", DEFAULT_REACTION_EMOJI) + + val quickReactions = rxPrefs.getString("quickReactions", DEFAULT_QUICK_REACTIONS) val messageSwipeRight = rxPrefs.getInteger("messageSwipeRight", MESSAGE_SWIPE_ACTION_REACT) val messageSwipeLeft = rxPrefs.getInteger("messageSwipeLeft", MESSAGE_SWIPE_ACTION_REPLY) val autoEmoji = rxPrefs.getBoolean("autoEmoji", true) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/EmojiPicker.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/EmojiPicker.kt new file mode 100644 index 00000000..a0e4dd0c --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/EmojiPicker.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2017 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package dev.octoshrimpy.quik.common.util + +import android.content.Context +import android.graphics.Paint +import android.view.Gravity +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetDialog +import dev.octoshrimpy.quik.R +import dev.octoshrimpy.quik.common.util.extensions.dpToPx +import dev.octoshrimpy.quik.common.util.extensions.resolveThemeColor + +/** + * A lightweight, dependency-free emoji picker. Renders a themed, scrollable grid of emoji in a + * bottom sheet so it follows Quik's light/dark/black themes (unlike androidx emoji2-emojipicker, + * which pulled in Guava and ignored the app theme — see project memory). + */ +object EmojiPicker { + + // Popular reactions shown first. These include emoji-presentation sequences (base char + + // U+FE0F) that must be spelled out to render in colour — plain codepoint iteration would + // show them as black-and-white text glyphs. + private val commonEmojis = listOf( + "❤️", "👍", "👎", "😂", "😮", "😢", "🙏", "🔥", "🎉", "👏", + "💯", "😍", "🥰", "😊", "🤔", "😎", "😭", "😅", "😡", "🤣", + "😘", "👀", "✨", "⭐", "✅", "❌", "✌️", "🤝", "🙌", "💪" + ) + + // Unicode blocks that default to emoji (colour) presentation without a variation selector, + // so they enumerate cleanly. Sequences/flags/skin-tones aren't covered by codepoint walking. + private val emojiRanges = listOf( + 0x1F300..0x1F5FF, // Misc Symbols and Pictographs + 0x1F600..0x1F64F, // Emoticons + 0x1F680..0x1F6FF, // Transport and Map + 0x1F900..0x1F9FF, // Supplemental Symbols and Pictographs + 0x1FA70..0x1FAFF // Symbols and Pictographs Extended-A + ) + + /** Common emoji first, then every renderable codepoint from the colour-emoji ranges. */ + val emojis: List by lazy { + val paint = Paint() + val generated = emojiRanges.asSequence() + .flatMap { it.asSequence() } + .mapNotNull { cp -> + val s = String(Character.toChars(cp)) + if (paint.hasGlyph(s)) s else null + } + (commonEmojis.asSequence() + generated).distinct().toList() + } + + /** + * Show the emoji grid in a bottom sheet. [onPicked] receives the chosen emoji and the sheet + * dismisses. [context] must be a themed (activity) context. + */ + fun show(context: Context, onPicked: (String) -> Unit) { + val backgroundColor = context.resolveThemeColor(android.R.attr.windowBackground) + val textColor = context.resolveThemeColor(android.R.attr.textColorPrimary) + val cell = 48.dpToPx(context) + val span = (context.resources.displayMetrics.widthPixels / cell).coerceAtLeast(6) + + val sheet = BottomSheetDialog(context) + + val recycler = RecyclerView(context).apply { + setBackgroundColor(backgroundColor) + layoutManager = GridLayoutManager(context, span) + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + (context.resources.displayMetrics.heightPixels * 0.5f).toInt() + ) + adapter = object : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + object : RecyclerView.ViewHolder(TextView(context).apply { + textSize = 22f + gravity = Gravity.CENTER + includeFontPadding = false + setTextColor(textColor) + setBackgroundResource(R.drawable.ripple) + layoutParams = RecyclerView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, cell + ) + }) {} + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val emoji = emojis[position] + (holder.itemView as TextView).text = emoji + holder.itemView.setOnClickListener { + onPicked(emoji) + sheet.dismiss() + } + } + + override fun getItemCount() = emojis.size + } + } + + sheet.setContentView(recycler) + sheet.show() + } +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt index 1669372d..afd4437d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt @@ -45,7 +45,6 @@ import android.view.MenuItem import android.view.Gravity import android.view.View import android.view.ViewGroup -import android.widget.EditText import android.widget.LinearLayout import android.widget.PopupWindow import android.widget.SeekBar @@ -75,6 +74,7 @@ import dev.octoshrimpy.quik.common.util.DateFormatter import dev.octoshrimpy.quik.common.util.extensions.autoScrollToStart import dev.octoshrimpy.quik.common.util.extensions.dpToPx import dev.octoshrimpy.quik.common.util.extensions.hideKeyboard +import dev.octoshrimpy.quik.common.util.EmojiPicker import dev.octoshrimpy.quik.common.util.extensions.makeToast import dev.octoshrimpy.quik.common.util.extensions.scrapViews import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint @@ -87,6 +87,7 @@ import dev.octoshrimpy.quik.feature.compose.editing.ChipsAdapter import dev.octoshrimpy.quik.feature.contacts.ContactsActivity import dev.octoshrimpy.quik.model.Attachment import dev.octoshrimpy.quik.model.Recipient +import dev.octoshrimpy.quik.util.Preferences import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable @@ -732,7 +733,12 @@ class ComposeActivity : QkThemedActivity(), ComposeView { } override fun showReactionPicker(messageId: Long) { - val defaultEmojis = listOf("❤️", "👍", "👎", "😂", "😮", "😢") + // User-configurable quick reactions (Settings → Quick reactions); fall back to defaults + val defaultEmojis = prefs.quickReactions.get() + .split(",") + .map { it.trim() } + .filter { it.isNotEmpty() } + .ifEmpty { Preferences.DEFAULT_QUICK_REACTIONS.split(",") } val theme = colors.theme() val vPad = 8.dpToPx(this) @@ -774,18 +780,9 @@ class ComposeActivity : QkThemedActivity(), ComposeView { row.addView(emojiView(emoji) { sendReactionIntent.onNext(Pair(messageId, emoji)) }) } - // "+" opens a simple input so the user can type or paste any emoji + // "+" opens the full scrollable emoji grid so any emoji can be chosen row.addView(emojiView("+") { - val input = EditText(this).apply { hint = getString(R.string.compose_reaction_custom_hint) } - AlertDialog.Builder(this) - .setTitle(R.string.compose_reaction_custom_title) - .setView(input) - .setPositiveButton(R.string.button_send) { _, _ -> - val emoji = input.text.toString().trim() - if (emoji.isNotEmpty()) sendReactionIntent.onNext(Pair(messageId, emoji)) - } - .setNegativeButton(R.string.button_cancel, null) - .show() + EmojiPicker.show(this) { emoji -> sendReactionIntent.onNext(Pair(messageId, emoji)) } }) // Anchor above the long-pressed message if its view is on screen; else center on the list @@ -794,17 +791,28 @@ class ComposeActivity : QkThemedActivity(), ComposeView { .takeIf { it != -1 } ?.let { binding.messageList.layoutManager?.findViewByPosition(it) } + row.measure( + View.MeasureSpec.makeMeasureSpec(popupWidth, View.MeasureSpec.EXACTLY), + View.MeasureSpec.UNSPECIFIED + ) + if (messageView != null) { - row.measure( - View.MeasureSpec.makeMeasureSpec(popupWidth, View.MeasureSpec.EXACTLY), - View.MeasureSpec.UNSPECIFIED - ) - popup.showAsDropDown(messageView, margin, -(messageView.height + row.measuredHeight)) + val loc = IntArray(2) + messageView.getLocationInWindow(loc) + // Neutralize any in-progress swipe-recovery translation so the popup is not + // placed off-screen. The popup is ~full width, so anchor x at a fixed margin + // rather than to the (possibly still translated) swiped row. + val top = loc[1] - messageView.translationY.toInt() + val y = (top - row.measuredHeight).let { above -> + if (above >= 0) above else top + messageView.height // flip below if no room above + } + popup.showAtLocation(binding.messageList, Gravity.NO_GRAVITY, margin, y) } else { popup.showAtLocation(binding.messageList, Gravity.CENTER, 0, 0) } } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.compose, menu) return super.onCreateOptionsMenu(menu) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt index 15414988..7aa89b18 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt @@ -23,6 +23,8 @@ import android.content.Context import android.graphics.Typeface import android.net.Uri import android.os.Build +import android.os.Handler +import android.os.Looper import android.text.Layout import android.text.Spannable import android.text.SpannableString @@ -35,6 +37,8 @@ import android.text.util.Linkify import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.animation.DecelerateInterpolator +import android.view.animation.OvershootInterpolator import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView @@ -103,6 +107,9 @@ class MessagesAdapter @Inject constructor( private const val MAX_MESSAGE_DISPLAY_LENGTH = 5000 private const val DOUBLE_TAP_TIMEOUT_MS = 300L + + // delay between each reaction badge's pulse when a conversation opens on a reaction + private const val REACTION_PULSE_STAGGER_MS = 250L } // tracks the previous tap so we can detect a double-tap without an OnTouchListener @@ -110,6 +117,13 @@ class MessagesAdapter @Inject constructor( private var lastTapMessageId = -1L private var lastTapTime = 0L + // When the conversation opens with a reaction as the newest message, the visible reaction + // badges pulse once, cascaded by REACTION_PULSE_STAGGER_MS. pulsedReactionIds keeps each + // badge to a single pulse despite RecyclerView rebinding. + private var pendingReactionPulse = false + private var reactionPulseOrder = 0 + private val pulsedReactionIds = mutableSetOf() + // click events passed back to compose view model val partClicks: Subject = PublishSubject.create() val messageLinkClicks: Subject = PublishSubject.create() @@ -128,6 +142,15 @@ class MessagesAdapter @Inject constructor( field = value contactCache.clear() + // Arm the reaction pulse if the newest message in the thread is a reaction. Binds for + // the initially-visible badges run synchronously on the next layout pass, so they all + // see the flag; the delayed reset only stops badges scrolled into view later. + if (value?.first?.lastMessage?.isEmojiReaction == true && !pendingReactionPulse) { + pendingReactionPulse = true + reactionPulseOrder = 0 + Handler(Looper.getMainLooper()).postDelayed({ pendingReactionPulse = false }, 1500) + } + updateData(value?.second) } @@ -145,6 +168,13 @@ class MessagesAdapter @Inject constructor( private val audioState = AudioState() + init { + // Stable ids so RecyclerView populates viewHolder.itemId from getItemId(); the swipe + // callback (MessageItemTouchCallback) reads itemId to identify the swiped message. + // Without this, itemId is NO_ID (-1) and swipe-to-react can't resolve the message. + setHasStableIds(true) + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { // Use the parent's context to inflate the layout, otherwise link clicks will crash the app val inflater = LayoutInflater.from(parent.context) @@ -469,12 +499,48 @@ class MessagesAdapter @Inject constructor( reactionTextView.text = reactionText reactionTextView.setOnClickListener { reactionClicks.onNext(message.id) } reactionsContainer.setVisible(true) + + // clear any leftover scale from a recycled view + reactionTextView.animate().cancel() + reactionTextView.scaleX = 1f + reactionTextView.scaleY = 1f + + // pulse once, cascaded, when the conversation opened on a reaction + if (pendingReactionPulse && pulsedReactionIds.add(message.id)) { + bounceReaction(reactionTextView, reactionPulseOrder++ * REACTION_PULSE_STAGGER_MS) + } } else { reactionsContainer.setVisible(false) reactionTextView.setOnClickListener(null) } } + /** Grow-and-shrink bounce: 1 → 1.4 → 0.9 → 1, after [startDelay] ms. */ + private fun bounceReaction(view: View, startDelay: Long) { + view.scaleX = 1f + view.scaleY = 1f + view.animate() + .scaleX(1.4f).scaleY(1.4f) + .setStartDelay(startDelay) + .setDuration(150) + .setInterpolator(DecelerateInterpolator()) + .withEndAction { + view.animate() + .scaleX(0.9f).scaleY(0.9f) + .setStartDelay(0) + .setDuration(120) + .withEndAction { + view.animate() + .scaleX(1f).scaleY(1f) + .setDuration(120) + .setInterpolator(OvershootInterpolator()) + .start() + } + .start() + } + .start() + } + private fun bindStatus( statusView: TextView, diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt index 3475725a..48eb6f66 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt @@ -24,9 +24,15 @@ import android.content.Context import android.os.Build import android.text.format.DateFormat import android.view.LayoutInflater +import android.view.Gravity import android.view.View import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView import com.bluelinelabs.conductor.RouterTransaction import com.google.android.material.snackbar.Snackbar import com.jakewharton.rxbinding2.view.clicks @@ -40,7 +46,10 @@ import dev.octoshrimpy.quik.common.QkChangeHandler import dev.octoshrimpy.quik.common.QkDialog import dev.octoshrimpy.quik.common.base.QkController import dev.octoshrimpy.quik.common.util.Colors +import dev.octoshrimpy.quik.common.util.EmojiPicker import dev.octoshrimpy.quik.common.util.extensions.animateLayoutChanges +import dev.octoshrimpy.quik.common.util.extensions.dpToPx +import dev.octoshrimpy.quik.common.util.extensions.resolveThemeColor import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint import dev.octoshrimpy.quik.common.util.extensions.setVisible import dev.octoshrimpy.quik.common.widget.PreferenceView @@ -56,6 +65,7 @@ import dev.octoshrimpy.quik.util.Preferences import io.reactivex.Observable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject +import java.util.Collections import javax.inject.Inject class SettingsController : QkController(), SettingsView { @@ -77,16 +87,12 @@ class SettingsController : QkController = PublishSubject.create() private val startTimeSelectedSubject: Subject> = PublishSubject.create() private val endTimeSelectedSubject: Subject> = PublishSubject.create() private val signatureSubject: Subject = PublishSubject.create() private val defaultReactionSubject: Subject = PublishSubject.create() + private val quickReactionsSubject: Subject = PublishSubject.create() private val progressAnimator by lazy { ObjectAnimator.ofInt(binding.syncingProgress, "progress", 0, 0) } @@ -147,6 +153,8 @@ class SettingsController : QkController = defaultReactionSubject.hide() + override fun quickReactionsChanged(): Observable = quickReactionsSubject.hide() + override fun mmsSizeSelected(): Observable = mmsSizeDialog.adapter.menuItemClicks override fun messageLinkHandlingSelected(): Observable = messageLinkHandlingDialog.adapter.menuItemClicks @@ -177,6 +185,8 @@ class SettingsController : QkController defaultReactionSubject.onNext(emoji) } + } + + override fun showQuickReactionsDialog(current: List) { + val ctx = activity!! + val count = Preferences.QUICK_REACTIONS_COUNT + val items = current.filter { it.isNotEmpty() }.toMutableList() + while (items.size < count) items.add("➕") + while (items.size > count) items.removeAt(items.lastIndex) + + val textColor = ctx.resolveThemeColor(android.R.attr.textColorPrimary) + val cell = 48.dpToPx(ctx) + + lateinit var slotAdapter: RecyclerView.Adapter + slotAdapter = object : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + object : RecyclerView.ViewHolder(TextView(ctx).apply { + textSize = 24f + gravity = Gravity.CENTER + includeFontPadding = false + setTextColor(textColor) + setBackgroundResource(R.drawable.ripple) + // each slot fills its grid column so all of them fit the dialog width + // (evenly spaced) without horizontal scrolling + layoutParams = RecyclerView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, cell + ) + }) {} + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + (holder.itemView as TextView).text = items[position] + // tap a slot to replace its emoji via the full picker + holder.itemView.setOnClickListener { + EmojiPicker.show(ctx) { emoji -> + items[holder.adapterPosition] = emoji + slotAdapter.notifyItemChanged(holder.adapterPosition) + } + } + } + + override fun getItemCount() = items.size + } + + val recycler = RecyclerView(ctx).apply { + // one row of equal-width columns -> all slots fit, evenly spaced, no scrolling + layoutManager = GridLayoutManager(ctx, count) + adapter = slotAdapter + setPadding(8.dpToPx(ctx), 16.dpToPx(ctx), 8.dpToPx(ctx), 16.dpToPx(ctx)) + clipToPadding = false + } + + // long-press drag to reorder the slots + ItemTouchHelper(object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val from = viewHolder.adapterPosition + val to = target.adapterPosition + Collections.swap(items, from, to) + slotAdapter.notifyItemMoved(from, to) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit + }).attachToRecyclerView(recycler) + + AlertDialog.Builder(ctx) + .setTitle(R.string.settings_quick_reactions_title) + .setMessage(R.string.settings_quick_reactions_dialog_hint) + .setView(recycler) + .setPositiveButton(R.string.button_save) { _, _ -> + quickReactionsSubject.onNext(items.joinToString(",") { it.ifEmpty { "➕" } }) + } + .setNegativeButton(R.string.button_cancel, null) + .show() + } override fun showMmsSizePicker() = mmsSizeDialog.show(activity!!) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsPresenter.kt index e2af3ba9..b86625bb 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsPresenter.kt @@ -99,6 +99,9 @@ class SettingsPresenter @Inject constructor( disposables += prefs.defaultReactionEmoji.asObservable() .subscribe { emoji -> newState { copy(defaultReactionEmoji = emoji) } } + disposables += prefs.quickReactions.asObservable() + .subscribe { quick -> newState { copy(quickReactions = quick) } } + val textSizeLabels = context.resources.getStringArray(R.array.text_sizes) disposables += prefs.textSize.asObservable() .subscribe { textSize -> @@ -189,6 +192,9 @@ class SettingsPresenter @Inject constructor( R.id.defaultReaction -> view.showDefaultReactionDialog(prefs.defaultReactionEmoji.get()) + R.id.quickReactions -> view.showQuickReactionsDialog( + prefs.quickReactions.get().split(",").map { it.trim() }.filter { it.isNotEmpty() }) + R.id.delayed -> view.showDelayDurationDialog() R.id.delivery -> prefs.delivery.set(!prefs.delivery.get()) @@ -288,6 +294,11 @@ class SettingsPresenter @Inject constructor( .autoDisposable(view.scope()) .subscribe() + view.quickReactionsChanged() + .doOnNext(prefs.quickReactions::set) + .autoDisposable(view.scope()) + .subscribe() + view.mmsSizeSelected() .autoDisposable(view.scope()) .subscribe(prefs.mmsSize::set) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt index 0c39ed7c..7fce051f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt @@ -37,6 +37,7 @@ data class SettingsState( val unreadAtTopEnabled: Boolean = false, val signature: String = "", val defaultReactionEmoji: String = Preferences.DEFAULT_REACTION_EMOJI, + val quickReactions: String = Preferences.DEFAULT_QUICK_REACTIONS, val textSizeSummary: String = "", val textSizeId: Int = Preferences.TEXT_SIZE_NORMAL, val systemFontEnabled: Boolean = false, diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsView.kt index 0656eab9..0078a91d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsView.kt @@ -33,6 +33,7 @@ interface SettingsView : QkViewContract { fun sendDelaySelected(): Observable fun signatureChanged(): Observable fun defaultReactionChanged(): Observable + fun quickReactionsChanged(): Observable fun mmsSizeSelected(): Observable fun messageLinkHandlingSelected(): Observable @@ -44,6 +45,7 @@ interface SettingsView : QkViewContract { fun showDelayDurationDialog() fun showSignatureDialog(signature: String) fun showDefaultReactionDialog(current: String) + fun showQuickReactionsDialog(current: List) fun showMmsSizePicker() fun showMessageLinkHandlingDialogPicker() fun showSwipeActions() diff --git a/presentation/src/main/res/layout/settings_controller.xml b/presentation/src/main/res/layout/settings_controller.xml index c774da8b..dbe850bb 100644 --- a/presentation/src/main/res/layout/settings_controller.xml +++ b/presentation/src/main/res/layout/settings_controller.xml @@ -166,6 +166,14 @@ app:icon="@drawable/ic_favorite_black_24dp" app:title="@string/settings_default_reaction_title" /> + + Speak Default reaction + Quick reactions + Emoji shown in the reaction popup + Tap to change, drag to reorder Message swipe actions Configure swipe actions for messages