feat: Send emoji reactions (react to messages, not just parse incoming) - #846
feat: Send emoji reactions (react to messages, not just parse incoming)#846guttermonk wants to merge 9 commits into
Conversation
Long-press (or double-tap, configurable) a message to react from a floating picker; sends Google Messages / iOS-readable wire format per recipient and records the reaction locally. Works on text, image, and audio messages.
The emoji row now scrolls with the '+' pinned so it stays reachable once recents push the row past the screen width. The custom-emoji dialog auto-opens the keyboard and submits on the first emoji typed, removing the extra taps.
When a message has multiple reactions, each distinct emoji now shows its own count (only when used more than once), e.g. "❤️2 👍", instead of listing emojis with a single combined total.
…picker Adds a 'Recent emojis in picker' setting (None–5, default 3) controlling how many recents show after the tapbacks; recents are now stored up to 10 so history survives count changes. The picker popup now uses a wrapping FlexboxLayout instead of horizontal scroll, so all emojis (and the '+') stay visible across multiple lines.
A TextView's text-colour alpha is applied to emoji glyphs, and the theme's default text colour is partially transparent, so the picker emojis looked dim. Force an opaque text colour on the chips.
Adds heart/send/history icons to the three reaction settings to match the existing rows. Renames 'Reaction gesture' to 'Message gestures' and makes each option (and the summary) spell out both gestures, e.g. 'Long-press to react, double-tap to select', so it's clear the choice swaps both.
Instead of greedily filling each row (eg. 8 + 1), the picker now gives chips a uniform width and computes a balanced chips-per-row = ceil(total / rowCount), so a wrapped picker splits evenly (eg. 5 + 4) and centers each row. Adapts to any recent-emoji count and screen width.
sendNewMessages now accepts an asReaction flag that marks the message isEmojiReaction=true before it's first committed to Realm. Previously the reaction message was inserted visible, the SMS sent, and only then hidden in a second transaction — so the raw 'Reacted …' bubble briefly flashed in the conversation on both add and remove.
…reactions The reaction bar's '+' now opens an androidx EmojiPickerView (categorised, scrollable, tracks its own recents) instead of a keyboard text field. Details: - bump com.google.android.material 1.0.0 -> 1.6.1 and add emoji2-emojipicker, so the picker can use a Material3 theme that follows the app's day/night setting - host it in a full-screen translucent dialog with a dim scrim and the drawer pinned to the bottom, giving the grid a bounded viewport that scrolls; a swipe starting above the drawer dismisses it (even if it travels into the drawer) while swipes within scroll - copy the activity's system-bar colours and legacy systemUiVisibility flags onto the dialog so the status/nav bars (colours and light/dark icons) stay matching the app in both light and dark mode - guard isEmojiOnly against EmojiCompat not being loaded yet (a startup crash surfaced by the new dependency's init timing) - drop the now-unused keyboard custom-emoji dialog strings
4aa3ad6 to
6d79578
Compare
|
why not just always send in the Google Messages format? all devices can parse it |
|
Good question — the problem is that the premise isn't quite true: iMessage/iOS can't parse the Google Messages format. That format carries the reaction in zero-width delimiters (zero-width space/non-joiner + hair spaces around the emoji). Google Messages and QUIK know to strip those and render a native reaction, but iMessage doesn't — so an iPhone recipient just sees the visible fallback text with a scattering of invisible characters embedded in it, which looks broken. If a user wants to only use the Google Message format, they can change "Reaction send format" from Automatic to Google Messages. That said, Automatic mode has been tested on devices using QUIK, gMessage, and iMessage and it works across all devices without issue. So, I prefer to give users the freedom to choose which format they want to use, which includes the Automatic mode. |
|
In that case why not always send the iOS format? |
|
Is it possible to just send emoji and the receiver get the bubble emoji on the message without visible text ("Reacted [emoji] to [text]") on both iMessage, Quik, and Google Messages, or will one always have the text on iMessages? |
|
Found possible issue. Reacted in a group chat, react messages sent to individual contacts. Bubble reactions still visible on my end in the group chat (QUIK) |
Because the iOS format has the opposite problem: it reads fine as text everywhere, but Google Messages won't render it as an actual reaction — it only understands its own zero-width format for that. So if we always sent iOS-readable text, every Google Messages recipient would stop getting a native reaction attached to the bubble and instead see a loose Loved "…" text message in the thread. So it's a genuine trade-off with no universal winner:
That's the whole reason for Automatic: send the Google format to threads that speak it (native reaction) and iOS-readable text to threads that don't (clean fallback), picking per-conversation based on what that person's app last sent. The manual options are just there for anyone who knows their contact's setup and wants to override the guess. Would it be better if the detected mode was displayed in the chat? |
Thanks for testing this, @eybmv — really helpful report. A few clarifications: Why Automatic matched Google Messages: Automatic only switches a conversation to iOS format after it has received an iOS-style reaction from that thread. Until then it defaults to Google — so if the iPhone user hadn't reacted first, you'd get the Google result either way. In a group chat that's even more likely, since it hadn't seen an iPhone reaction to match against. Why a mixed group can't be perfect: SMS/MMS sends one message body to the whole thread, so QUIK picks a single format per reaction — it can't send iOS text to the iPhone and Google format to the Android in the same message. Automatic follows whoever reacted last in that thread:
So it can flip back and forth as new reactions come in, and whichever it lands on, one participant gets the worse rendering. Forcing the format manually is the only way to make it deterministic — but it's still one format for the whole group, so you're really just choosing which platform gets the clean experience. If there's a better way to handle this, I'm all ears — pull requests are more than welcome. It's too bad big tech never agreed on a universal standard instead of putting us in this spot. |
|
Right now the reaction format (iPhone-style text vs. native Android reaction) is a single global setting, and in Automatic mode it's invisible which one a given chat will use — it just follows whoever reacted last. That's confusing, and it can't be corrected when you know something the app doesn't (e.g. the other person is on an iPhone but hasn't reacted yet). Proposal: add a per-chat reaction format that can be viewed, and adjusted if needed.
This wouldn't change the underlying limitation though — SMS/MMS still sends one format per message, so in a mixed iPhone+Android group you're choosing which side gets the clean version — and reactions to iPhones still arrive as text, not a native bubble. It just makes the choice visible and lets you set it deliberately instead of guessing. Would this be useful, or overkill that clutters up the UI? Any thoughts on where the indicator should live (top of the chat vs. inside the reaction picker vs. in the top-right kebab menu)? |
There was a problem hiding this comment.
Pull request overview
Adds the ability to send emoji reactions (not just parse/display incoming ones), including a floating reaction picker, configurable gestures and send wire-format selection, plus persistence/migration changes so reactions render immediately and can auto-detect the best outgoing format per conversation.
Changes:
- Adds reaction sending pipeline (
SendReaction,MessageRepository.sendReaction(...)) and extends reaction parsing/storage to trackfromMeand wireformat. - Introduces reaction UI: floating tapback/recent emoji picker, full emoji grid picker (AndroidX
EmojiPickerView), and media-part “React” context action. - Adds Settings → Reactions preferences (gesture binding, send format, recents count) and bumps Realm schema + Material/EmojiPicker dependencies.
Reviewed changes
Copilot reviewed 28 out of 29 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| presentation/src/main/res/values/themes.xml | Adds a Material3 DayNight dialog theme for the emoji grid picker. |
| presentation/src/main/res/values/strings.xml | Adds reaction UI strings and Settings arrays/IDs for reaction preferences. |
| presentation/src/main/res/menu/mms_part_menu.xml | Adds “React” action for MMS part context menu. |
| presentation/src/main/res/layout/settings_controller.xml | Adds Settings rows for reaction gesture/format/recents. |
| presentation/src/main/res/layout/reaction_bar.xml | Adds floating reaction bar container layout (Flexbox). |
| presentation/src/main/res/layout/reaction_bar_emoji.xml | Adds individual emoji “chip” layout for the reaction bar. |
| presentation/src/main/res/drawable/reaction_bar_background.xml | Adds rounded background for the reaction picker. |
| presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsView.kt | Adds intents + dialog methods for reaction settings. |
| presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt | Adds state fields/summaries for reaction settings. |
| presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsPresenter.kt | Wires prefs to state + click handling for reaction settings. |
| presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt | Hooks dialogs/adapters/rendering for reaction settings. |
| presentation/src/main/java/com/moez/QKSMS/feature/extensions/CharSequenceExtensions.kt | Guards EmojiCompat usage to avoid “Not initialized yet” crashes. |
| presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt | Adds gesture logic (double-tap vs long-press) + per-emoji reaction counts. |
| presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt | Sends/toggles reactions from UI intents and labels “You” for local reactions. |
| presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt | Adds reactionSelectedIntent for picker selection events. |
| presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt | Implements floating reaction picker UI and Material3 emoji grid drawer. |
| presentation/src/main/java/com/moez/QKSMS/common/base/QkRealmAdapter.kt | Adds isSelectionMode() helper used by the new gesture logic. |
| presentation/build.gradle | Adds AndroidX emoji picker dependency. |
| domain/src/main/java/com/moez/QKSMS/util/Preferences.kt | Adds reaction prefs + constants. |
| domain/src/main/java/com/moez/QKSMS/repository/MessageRepository.kt | Adds sendReaction(...) and sendNewMessages(..., asReaction) flag. |
| domain/src/main/java/com/moez/QKSMS/repository/EmojiReactionRepository.kt | Adds outgoing reaction body builder + per-thread format resolver. |
| domain/src/main/java/com/moez/QKSMS/model/EmojiReaction.kt | Adds fromMe and format fields + format constants. |
| domain/src/main/java/com/moez/QKSMS/interactor/SendReaction.kt | New interactor for sending reactions and updating conversations. |
| data/src/main/java/com/moez/QKSMS/util/EmojiPatternStrings.kt | Adds locale-configurable outgoing iOS template fields. |
| data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt | Implements sendReaction(...) and hides reaction messages immediately. |
| data/src/main/java/com/moez/QKSMS/repository/EmojiReactionRepositoryImpl.kt | Implements reaction body generation + auto format resolution + fromMe tracking. |
| data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt | Bumps schema to v16 and migrates new EmojiReaction fields. |
| data/src/main/assets/emojis/en.json | Adds English outgoing iOS templates. |
| build.gradle | Bumps Material Components version. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| val target = getUnmanagedMessage(targetMessageId) ?: return listOf() | ||
| val targetText = target.getText(false) |
| // Send it through the normal pipeline (delivery, retry, etc. all handled). asReaction hides | ||
| // the message from the moment it's created so the raw reaction text never flashes on screen. | ||
| val sent = sendNewMessages(subId, listOf(target.address), body, listOf(), false, 0, asReaction = true) | ||
|
|
| val runnable = Runnable { onSingleTap(id, view, status) } | ||
| pendingSingleTap = runnable | ||
| view.postDelayed(runnable, doubleTapTimeout) |
incorrect, Google Messages parses iOS reactions and renders them. there's a setting, enabled by default |
have you tested this and confirmed? |
|
Please read: #152 (comment) and #152 (comment), @benwaffle is correct. It doesn't matter. |
|
fantastic news. this may be the path forward, then, I'd think. |
I think you misread @eybmv's report. Could be wrong, but I think they're saying they sent a reaction in a group chat, and instead of it sending via the GC it sent it to each person individually. So I'd guess the reaction code isn't respecting the "Send as GC/individually" toggle at the top of the compose activity. |
Send emoji reactions
QUIK could already parse and display incoming reactions (iMessage tapbacks and Google Messages reactions). This PR adds the missing half: sending reactions from within QUIK, so you can react to a message instead of only seeing reactions others send.
Closes #838
Closes #152
Not just parity — QUIK goes further
This brings QUIK to parity with iMessage and Google Messages for reacting, and adds a couple of things they don't offer over SMS/MMS:
Loved "…") instead of the Google format's stray invisible characters. No other SMS app we're aware of lets you choose or auto-match the reaction wire format.Full feature list
❤️2 👍) rather than one combined total.Implementation notes
fromMeandformatfields to theEmojiReactionmodel (Realm schema bump to v16, with migration).SendReactioninteractor and aMessageRepository.sendReaction(...)entry point;EmojiReactionRepositorygainsbuildReactionBody(...)andresolveFormat(...).EmojiPickerView. To theme it correctly (it reads Material3 colour attributes) this bumpscom.google.android.material1.0.0 → 1.6.1and addsandroidx.emoji2:emoji2-emojipicker. It's shown in a themed, dismissible drawer that follows the app's day/night setting. Only Design-heritage material widgets are used elsewhere (AppBar/CollapsingToolbar/FAB/Snackbar), which don't require a MaterialComponents theme.Testing
Built and manually tested on a debug build across: reacting/un-reacting to text, image and audio messages; the gesture toggle; the + emoji-grid picker (scroll, swipe-to-dismiss, day/night theming); recents and the configurable count; balanced wrapping; per-emoji counts; and round-tripping the Google-format reactions QUIK↔QUIK.
🤖 Generated with Claude Code