chahinebrini b31066a04c feat(chat): native action sheet + Insta-style heart for DM messages
- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet)
- DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked
- Group chat unchanged
- Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state
- deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle
- NEXT_RELEASE.md: DM reactions release note
- Includes other staged work across binder-mac, marketing, ops/mdm, ios/
2026-05-30 09:14:32 +02:00

266 lines
7.9 KiB
TypeScript

import {
View,
Text,
Pressable,
TouchableOpacity,
Modal,
Platform,
StyleSheet,
useColorScheme,
} from 'react-native';
import { BlurView } from 'expo-blur';
import { useRouter, type RelativePathString } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../../stores/auth';
import { useColors } from '../../lib/theme';
const IS_IOS = Platform.OS === 'ios';
// Controlled-Modal-Pattern. Trigger ist NICHT in dieser Komponente — der
// Avatar im AppHeader öffnet das Modal via `visible`-Prop (User-Anweisung
// 2026-05-07: kein separates 3-Punkte-Icon).
//
// Card-Style mit:
// - SOS prominent oben (nur Wort "SOS" rot, Tagline neutral; ernste Sache,
// nicht mit Gaming/Profile in eine Liste werfen)
// - Profile · Settings · Games · [Debug DEV] in der Mitte
// - Abmelden unten, neutral (nicht rot — Recovery-tonal, kein Alarm)
type ItemKey = 'profile' | 'settings' | 'games' | 'debug';
type Item = {
key: ItemKey;
label: string;
icon: React.ComponentProps<typeof Ionicons>['name'];
onSelect: () => void | Promise<void>;
};
type Props = {
visible: boolean;
onClose: () => void;
topOffset?: number;
};
export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props) {
const router = useRouter();
const { t } = useTranslation();
const { signOut } = useAuthStore();
const colors = useColors();
const scheme = useColorScheme();
function nav(path: RelativePathString) {
onClose();
router.push(path);
}
async function handleLogout() {
onClose();
await signOut();
router.replace('/' as RelativePathString);
}
const items: Item[] = [
{
key: 'profile',
label: t('headerMenu.profile'),
icon: 'person-outline',
onSelect: () => nav('/profile' as RelativePathString),
},
{
key: 'settings',
label: t('headerMenu.settings'),
icon: 'settings-outline',
onSelect: () => nav('/settings' as RelativePathString),
},
{
key: 'games',
label: t('headerMenu.games'),
icon: 'game-controller-outline',
onSelect: () => nav('/games' as RelativePathString),
},
];
// Debug-Eintrag: __DEV__ immer, plus TestFlight/production-Builds wenn
// EXPO_PUBLIC_ENABLE_DEBUG gesetzt ist (eas.json production-Profil).
if (__DEV__ || process.env.EXPO_PUBLIC_ENABLE_DEBUG === '1') {
items.push({
key: 'debug',
label: t('headerMenu.debug'),
icon: 'bug-outline',
onSelect: () => nav('/debug' as RelativePathString),
});
}
return (
<Modal
visible={visible}
transparent
animationType="fade"
statusBarTranslucent
onRequestClose={onClose}
>
<Pressable
onPress={onClose}
style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.18)' }}
>
<View
onStartShouldSetResponder={() => true}
style={{
position: 'absolute',
top: topOffset,
right: 12,
// iOS: transparent → BlurView liefert die frosted Fläche (natives
// Menü-Material). Android: solider Surface-Hintergrund.
backgroundColor: IS_IOS ? 'transparent' : colors.surface,
borderRadius: 18,
shadowColor: '#000',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.18,
shadowRadius: 20,
elevation: 12,
minWidth: 210,
overflow: 'hidden',
}}
>
{IS_IOS ? (
<BlurView
intensity={85}
tint={scheme === 'dark' ? 'systemThickMaterialDark' : 'systemThickMaterialLight'}
style={StyleSheet.absoluteFill}
/>
) : null}
{/* SOS prominent — separat, ernst-tonal, nur "SOS" rot */}
<Pressable
onPress={() => {
onClose();
router.push('/urge' as RelativePathString);
}}
android_ripple={{ color: '#fee2e2' }}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 18,
paddingVertical: 16,
}}
>
<View
style={{
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#fee2e2',
alignItems: 'center',
justifyContent: 'center',
marginRight: 14,
}}
>
<Ionicons name="heart" size={18} color="#dc2626" />
</View>
<View style={{ flex: 1 }}>
<Text
style={{
fontSize: 15,
fontFamily: 'Nunito_700Bold',
color: '#dc2626',
}}
numberOfLines={1}
>
{t('appHeader.sosLabel')}
</Text>
<Text
style={{
fontSize: 12,
fontFamily: 'Nunito_400Regular',
color: colors.textMuted,
marginTop: 1,
}}
numberOfLines={1}
>
{t('appHeader.sosTagline')}
</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={colors.border} />
</View>
</Pressable>
<View style={{ height: 1, backgroundColor: colors.border }} />
{/* Profile · Settings · Games · [Debug DEV] */}
{items.map((item) => (
<TouchableOpacity
key={item.key}
onPress={() => {
onClose();
void item.onSelect();
}}
activeOpacity={0.7}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 18,
paddingVertical: 14,
}}
>
<Ionicons
name={item.icon}
size={18}
color={colors.textMuted}
style={{ marginRight: 14 }}
/>
<Text
style={{
fontSize: 14,
fontFamily: 'Nunito_600SemiBold',
color: colors.text,
}}
>
{item.label}
</Text>
</View>
</TouchableOpacity>
))}
<View style={{ height: 1, backgroundColor: colors.border }} />
{/* Abmelden — neutral, nicht rot */}
<TouchableOpacity
onPress={handleLogout}
activeOpacity={0.7}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 18,
paddingVertical: 14,
}}
>
<Ionicons
name="log-out-outline"
size={18}
color={colors.textMuted}
style={{ marginRight: 14 }}
/>
<Text
style={{
fontSize: 14,
fontFamily: 'Nunito_600SemiBold',
color: colors.text,
}}
>
{t('headerMenu.logout')}
</Text>
</View>
</TouchableOpacity>
</View>
</Pressable>
</Modal>
);
}