feat(mobile): ReactionStrip quick-reactor + ReactionPills display
This commit is contained in:
@@ -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 (
|
||||||
|
<View style={styles.row}>
|
||||||
|
{grouped.map((g) => (
|
||||||
|
<Pressable
|
||||||
|
key={g.emoji}
|
||||||
|
onPress={() => onToggle(g.emoji)}
|
||||||
|
style={[styles.pill, g.mine && styles.pillMine]}
|
||||||
|
>
|
||||||
|
<Text style={styles.emoji}>{g.emoji}</Text>
|
||||||
|
<Text style={[styles.count, g.mine && styles.countMine]}>{g.count}</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupByEmoji(
|
||||||
|
rows: MessageReaction[],
|
||||||
|
myUserId: string | null,
|
||||||
|
): Array<{ emoji: string; count: number; mine: boolean }> {
|
||||||
|
const m = new Map<string, { count: number; mine: boolean }>();
|
||||||
|
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 },
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<View style={styles.row}>
|
||||||
|
{QUICK_REACTIONS.map((e) => (
|
||||||
|
<Pressable
|
||||||
|
key={e}
|
||||||
|
onPress={() => onReact(e)}
|
||||||
|
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
|
||||||
|
hitSlop={6}
|
||||||
|
>
|
||||||
|
<Text style={styles.emoji}>{e}</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 },
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user