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:
parent
f83a13ba60
commit
89775838bc
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user