diff --git a/apps/mobile/components/ReactionPills.tsx b/apps/mobile/components/ReactionPills.tsx
new file mode 100644
index 0000000..39b449d
--- /dev/null
+++ b/apps/mobile/components/ReactionPills.tsx
@@ -0,0 +1,67 @@
+import type { MessageReaction } from '@chat-app/shared/chat';
+import { useMemo } from 'react';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { colors } from '../theme/colors';
+
+interface Props {
+ reactions: MessageReaction[];
+ myUserId: string | null;
+ onToggle: (emoji: string) => void;
+}
+
+// Renders the per-message reaction badges underneath a bubble. Pills
+// the current user has reacted to get a tinted background so they know
+// which ones to tap to un-react.
+export function ReactionPills({ reactions, myUserId, onToggle }: Props) {
+ const grouped = useMemo(() => groupByEmoji(reactions, myUserId), [reactions, myUserId]);
+ if (grouped.length === 0) return null;
+ return (
+
+ {grouped.map((g) => (
+ onToggle(g.emoji)}
+ style={[styles.pill, g.mine && styles.pillMine]}
+ >
+ {g.emoji}
+ {g.count}
+
+ ))}
+
+ );
+}
+
+function groupByEmoji(
+ rows: MessageReaction[],
+ myUserId: string | null,
+): Array<{ emoji: string; count: number; mine: boolean }> {
+ const m = new Map();
+ for (const r of rows) {
+ const prev = m.get(r.emoji) ?? { count: 0, mine: false };
+ m.set(r.emoji, {
+ count: prev.count + 1,
+ mine: prev.mine || (myUserId !== null && r.userId === myUserId),
+ });
+ }
+ return Array.from(m.entries()).map(([emoji, v]) => ({ emoji, ...v }));
+}
+
+const styles = StyleSheet.create({
+ row: { flexDirection: 'row', flexWrap: 'wrap', gap: 4, marginTop: 4 },
+ pill: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 8,
+ paddingVertical: 2,
+ borderRadius: 12,
+ backgroundColor: colors.surface,
+ borderColor: colors.border,
+ borderWidth: 1,
+ gap: 4,
+ },
+ pillMine: { backgroundColor: colors.accentMuted, borderColor: colors.accent },
+ emoji: { fontSize: 13 },
+ count: { color: colors.textMuted, fontSize: 12, fontWeight: '600' },
+ countMine: { color: colors.accent },
+});
diff --git a/apps/mobile/components/ReactionStrip.tsx b/apps/mobile/components/ReactionStrip.tsx
new file mode 100644
index 0000000..7d2ecf5
--- /dev/null
+++ b/apps/mobile/components/ReactionStrip.tsx
@@ -0,0 +1,45 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { colors } from '../theme/colors';
+
+// Hardcoded 6-emoji quick reactor shown at the top of the long-press
+// sheet. A full emoji picker is post-Phase-4; this is the Discord-style
+// fast path the vast majority of reactions go through.
+export const QUICK_REACTIONS = ['👍', '❤️', '😂', '😮', '😢', '🎉'] as const;
+
+export function ReactionStrip({ onReact }: { onReact: (emoji: string) => void }) {
+ return (
+
+ {QUICK_REACTIONS.map((e) => (
+ onReact(e)}
+ style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
+ hitSlop={6}
+ >
+ {e}
+
+ ))}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ row: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ paddingVertical: 12,
+ paddingHorizontal: 8,
+ borderBottomColor: colors.border,
+ borderBottomWidth: 1,
+ },
+ button: {
+ width: 44,
+ height: 44,
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 22,
+ },
+ buttonPressed: { backgroundColor: colors.accentMuted },
+ emoji: { fontSize: 26 },
+});