fix(chat): gedrückte Bubble scharf über Blur + Emoji-Ring entfernt

- MessageActionMenu: scharfe Preview-Kopie der gedrückten Bubble am Anker
  (bleibt über dem Blur sichtbar, WhatsApp-Stil) statt mitgeblurrt
- Reaktions-Leiste: kein Ring/Hintergrund mehr, aktives Emoji nur leicht größer
- Reaction-Pills: plain Emoji + Count ohne Hintergrund/Border

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-30 11:44:41 +02:00
parent f83a13ba60
commit 89775838bc
2 changed files with 60 additions and 30 deletions

View File

@ -131,6 +131,26 @@ export function ChatBubble({
function copyContent() { function copyContent() {
if (msg.content) Clipboard.setStringAsync(msg.content); if (msg.content) Clipboard.setStringAsync(msg.content);
} }
// Scharfe Kopie der Bubble fürs Kontextmenü (bleibt über dem Blur sichtbar).
const previewNode = (
<View
style={[
styles.bubble,
msg.isOwn ? ownBubbleRadius : otherBubbleRadius,
{ backgroundColor: bubbleBg },
!msg.isOwn && styles.bubbleOtherBorder,
isImageOnly && { padding: 4 },
]}
>
{msg.attachmentUrl && msg.attachmentType === 'image' ? (
<Image source={{ uri: msg.attachmentUrl }} style={styles.image} contentFit="cover" />
) : msg.content !== '' ? (
<Text style={[styles.content, { color: bubbleText }]}>{msg.content}</Text>
) : null}
</View>
);
return ( return (
<> <>
<View <View
@ -350,16 +370,10 @@ export function ChatBubble({
<TouchableOpacity <TouchableOpacity
key={r.emoji} key={r.emoji}
onPress={() => onReact?.(msg, r.emoji)} onPress={() => onReact?.(msg, r.emoji)}
activeOpacity={0.7} activeOpacity={0.6}
style={[ style={styles.reactionPill}
styles.reactionPill,
{
backgroundColor: colors.surfaceElevated,
borderColor: r.mine ? colors.brandOrange : colors.border,
},
]}
> >
<Text style={{ fontSize: 13 }}>{r.emoji}</Text> <Text style={{ fontSize: 16 }}>{r.emoji}</Text>
{r.count > 1 && <Text style={styles.reactionPillCount}>{r.count}</Text>} {r.count > 1 && <Text style={styles.reactionPillCount}>{r.count}</Text>}
</TouchableOpacity> </TouchableOpacity>
))} ))}
@ -374,6 +388,7 @@ export function ChatBubble({
isOwn={msg.isOwn} isOwn={msg.isOwn}
hasContent={hasContent} hasContent={hasContent}
myReaction={myReaction} myReaction={myReaction}
preview={previewNode}
onClose={() => setMenuVisible(false)} onClose={() => setMenuVisible(false)}
onReact={(emoji) => onReact?.(msg, emoji)} onReact={(emoji) => onReact?.(msg, emoji)}
onReply={() => onReply(msg)} onReply={() => onReply(msg)}
@ -405,10 +420,7 @@ function makeStyles(colors: ReturnType<typeof useColors>) {
reactionPill: { reactionPill: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderRadius: 12, paddingHorizontal: 2,
borderWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 7,
paddingVertical: 2,
}, },
reactionPillCount: { reactionPillCount: {
fontSize: 11, fontSize: 11,

View File

@ -8,7 +8,7 @@
* Blur-Backdrop (iOS) / semi-transparent (Android). Smart-Position: Menü unter * Blur-Backdrop (iOS) / semi-transparent (Android). Smart-Position: Menü unter
* der Bubble, oder darüber wenn unten kein Platz ist. * der Bubble, oder darüber wenn unten kein Platz ist.
*/ */
import { useMemo } from 'react'; import { useMemo, type ReactNode } from 'react';
import { import {
Dimensions, Dimensions,
Modal, Modal,
@ -37,6 +37,8 @@ type Props = {
hasContent: boolean; hasContent: boolean;
/** Aktuelles eigenes Reaktions-Emoji auf dieser Message (für Highlight). */ /** Aktuelles eigenes Reaktions-Emoji auf dieser Message (für Highlight). */
myReaction?: string | null; myReaction?: string | null;
/** Scharfe Kopie der gedrückten Bubble — bleibt über dem Blur sichtbar (WA-Stil). */
preview?: ReactNode;
onClose: () => void; onClose: () => void;
onReact: (emoji: string) => void; onReact: (emoji: string) => void;
onReply: () => void; onReply: () => void;
@ -50,6 +52,7 @@ export function MessageActionMenu({
isOwn, isOwn,
hasContent, hasContent,
myReaction, myReaction,
preview,
onClose, onClose,
onReact, onReact,
onReply, onReply,
@ -117,25 +120,39 @@ export function MessageActionMenu({
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.28)' }]} /> <View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.28)' }]} />
)} )}
{/* Scharfe Kopie der gedrückten Bubble — bleibt über dem Blur sichtbar (WA-Stil) */}
{preview && (
<View
pointerEvents="none"
style={[
{ position: 'absolute', top: anchor.y, width: anchor.width },
isOwn
? { right: Math.max(12, screenW - (anchor.x + anchor.width)) }
: { left: Math.max(12, anchor.x) },
]}
>
{preview}
</View>
)}
{/* Emoji-Reaktions-Leiste (nur fremde Nachrichten) */} {/* Emoji-Reaktions-Leiste (nur fremde Nachrichten) */}
{showReactions && ( {showReactions && (
<View style={[styles.reactionBar, sideStyle, { top: barTop, backgroundColor: colors.surface }]}> <View style={[styles.reactionBar, sideStyle, { top: barTop, backgroundColor: colors.surface }]}>
{REACTION_EMOJIS.map((emoji) => { {REACTION_EMOJIS.map((emoji) => (
const active = myReaction === emoji; <TouchableOpacity
return ( key={emoji}
<TouchableOpacity activeOpacity={0.5}
key={emoji} onPress={() => {
activeOpacity={0.6} onReact(emoji);
onPress={() => { onClose();
onReact(emoji); }}
onClose(); style={styles.reactionBtn}
}} >
style={[styles.reactionBtn, active && { backgroundColor: colors.surfaceElevated }]} <Text style={[styles.reactionEmoji, myReaction === emoji && styles.reactionEmojiActive]}>
> {emoji}
<Text style={styles.reactionEmoji}>{emoji}</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
); ))}
})}
</View> </View>
)} )}
@ -194,6 +211,7 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
}, },
reactionEmoji: { fontSize: 26 }, reactionEmoji: { fontSize: 26 },
reactionEmojiActive: { fontSize: 30 },
menu: { menu: {
position: 'absolute', position: 'absolute',
minWidth: 200, minWidth: 200,