Skip to content

feat: Send emoji reactions (react to messages, not just parse incoming) - #846

Open
guttermonk wants to merge 9 commits into
quik-sms:masterfrom
guttermonk:emoji-reactions
Open

feat: Send emoji reactions (react to messages, not just parse incoming)#846
guttermonk wants to merge 9 commits into
quik-sms:masterfrom
guttermonk:emoji-reactions

Conversation

@guttermonk

@guttermonk guttermonk commented Jul 22, 2026

Copy link
Copy Markdown

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:

  • Cross-app reaction interoperability with per-conversation auto-detection. QUIK can send in either the Google Messages (zero-width) format or human-readable iOS tapback text, and an Automatic mode mirrors whatever format was last received in that thread. So Android↔Android (Google Messages / QUIK) renders as a native reaction, while iPhone recipients get clean readable text (e.g. 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.
  • Configurable react/select gesture. Choose whether long-press reacts and double-tap multi-selects, or vice-versa — gesture customisation for reactions that iMessage and Google Messages don't offer.
  • Configurable recent-emoji count. Show 0–5 of your most recently used emojis in the picker, right after the standard tapbacks.

Full feature list

  • Floating reaction picker anchored above the bubble (iMessage / Google-style), on long-press by default.
  • Standard tapback quick row (❤️ 👍 👎 😂 ‼️ ❓), followed by your recent emojis, plus a that opens a full emoji grid picker (categorised, scrollable, tracks its own recents) for any emoji.
  • React to text, image, and audio messages — media via the message's context menu, not just text.
  • Add or remove a reaction by tapping — tapping the same emoji again removes it.
  • Your reaction is recorded locally and shown instantly, and sent over SMS/MMS so the recipient's app can render it.
  • Reaction badge shows a per-emoji count (e.g. ❤️2 👍) rather than one combined total.
  • The quick-reaction picker wraps onto balanced rows when it gets wide, instead of leaving a near-empty last row.
  • New Settings → Reactions: Message gestures, Reaction send format (Automatic / Google Messages / iOS-readable), and Recent emojis in picker, each with an icon matching the existing rows.

Implementation notes

  • Reactions are sent as a normal (hidden) SMS/MMS message in the chosen wire format, reusing the existing send pipeline (delivery, retry, delay all apply). The reaction message is marked hidden the moment it's written to Realm, so the raw 'Reacted …' bubble never flashes in the conversation.
  • Adds fromMe and format fields to the EmojiReaction model (Realm schema bump to v16, with migration).
  • New SendReaction interactor and a MessageRepository.sendReaction(...) entry point; EmojiReactionRepository gains buildReactionBody(...) and resolveFormat(...).
  • iOS-readable outgoing templates are English for now (incoming parsing remains fully localised); other locales fall back to English.
  • The full emoji grid picker uses AndroidX EmojiPickerView. To theme it correctly (it reads Material3 colour attributes) this bumps com.google.android.material 1.0.0 → 1.6.1 and adds androidx.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

guttermonk added 8 commits July 21, 2026 19:54
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
@guttermonk
guttermonk marked this pull request as ready for review July 24, 2026 01:06
@benwaffle

Copy link
Copy Markdown
Contributor

why not just always send in the Google Messages format? all devices can parse it

@guttermonk

Copy link
Copy Markdown
Author

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.

@benwaffle

Copy link
Copy Markdown
Contributor

In that case why not always send the iOS format?

@eybmv

eybmv commented Jul 28, 2026

Copy link
Copy Markdown

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?

@eybmv

eybmv commented Jul 29, 2026

Copy link
Copy Markdown

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)

@guttermonk

Copy link
Copy Markdown
Author

In that case why not always send the iOS format?

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:

  • Always Google format → clean native reactions on Android, but garbled invisible chars for iPhones.
  • Always iOS format → readable everywhere, but Google Messages users lose the native reaction and get an extra text bubble.

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?

@guttermonk

Copy link
Copy Markdown
Author

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)

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:

  • iPhone reacted last → iOS format for everyone: clean for the iPhone, but the Android/Google Messages user sees a plain Loved "…" text bubble instead of a native reaction.
  • Android reacted last → Google format for everyone: native reaction for the Android user, but zero-width-laden text for the iPhone.

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.

@guttermonk

Copy link
Copy Markdown
Author

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.

  • Each chat displays what Automatic has picked — e.g. "Reactions: Android" or "Reactions: iPhone" — in plain, recipient-friendly terms.
  • Tap it to override for that chat: Automatic / Android / iPhone.
  • An override is sticky (it won't silently flip back), with a one-tap "reset to Automatic."

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)?

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 track fromMe and wire format.
  • 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.

Comment on lines +701 to +702
val target = getUnmanagedMessage(targetMessageId) ?: return listOf()
val targetText = target.getText(false)
Comment on lines +709 to +712
// 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)

Comment on lines +194 to +196
val runnable = Runnable { onSingleTap(id, view, status) }
pendingSingleTap = runnable
view.postDelayed(runnable, doubleTapTimeout)
@benwaffle

benwaffle commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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

incorrect, Google Messages parses iOS reactions and renders them. there's a setting, enabled by default

@octoshrimpy

Copy link
Copy Markdown
Collaborator

incorrect, Google Messages parses iOS reactions and renders them. there's a setting, enabled by default

have you tested this and confirmed?

@Inhishonor

Copy link
Copy Markdown
Collaborator

Please read: #152 (comment) and #152 (comment), @benwaffle is correct. It doesn't matter.

@octoshrimpy

Copy link
Copy Markdown
Collaborator

fantastic news. this may be the path forward, then, I'd think.

@wiggleforlife

Copy link
Copy Markdown
Contributor

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)

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:

* iPhone reacted last → iOS format for everyone: clean for the iPhone, but the Android/Google Messages user sees a plain Loved "…" text bubble instead of a native reaction.

* Android reacted last → Google format for everyone: native reaction for the Android user, but zero-width-laden text for the iPhone.

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.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✏️ [ FEAT REQ ] Emoji Reactions ✏️ [ FEAT REQ ] Ability to react to messages with emojis

7 participants