190352820c
Bugs: - App Mute-/Auto-Playback: onMessage-Closure hielt stale ttsDeviceEnabled/ ttsMuted → Mute wurde ignoriert + AsyncStorage-Load kam nicht durch. Fix via ttsCanPlayRef (live gespiegelt) statt Closure-Variablen. - App Zombie-Recording: toggleWakeWord hat die laufende Aufnahme nicht gestoppt → audioService.recordingState blieb 'recording' → normaler Aufnahme-Button wirkungslos. Fix: await stopRecording() vor stop(). - Porcupine robuster: BuiltInKeywords-Enum Mapping mit String-Fallback, errorCallback fuer Runtime-Crashes (state zurueck auf off statt App-Crash), mehr Logging damit man beim naechsten Issue debuggen kann. App-Features: - MessageText Komponente: Text ist durchgehend selektierbar, erkennt URLs (http/https), E-Mails, Telefonnummern und macht sie anklickbar (oeffnet Browser / Mail-App / Android-Dialer via Linking). - TTS-Wiedergabegeschwindigkeit pro Geraet einstellbar (Settings -> "Sprechgeschwindigkeit", 0.5-2.0 in 0.1-Schritten, Default 1.0). Wird als speed-Param an die F5-TTS-Bridge durchgereicht. Bridge-Durchreichen: - ChatScreen: speed aus AsyncStorage via ttsSpeedRef, an chat/audio/ tts_request mitgeschickt - aria-bridge: _next_speed_override wie voice_override, an xtts_request weitergereicht - f5tts-bridge: speed-Param an F5TTS.infer() durchgereicht Diagnostic-Feature: - Voice-Preview-Button (Play-Icon) vor dem Delete-X in der Stimmen-Liste - Modal mit Textfeld (Default-Beispieltext wird bei jedem Oeffnen neu gesetzt) und Play-Button - Server sammelt audio_pcm Frames der Preview-Anfrage, baut WAV, schickt base64 zurueck, Browser spielt im <audio>-Tag ab - 60s Timeout-Safety-Net Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
91 lines
2.9 KiB
TypeScript
91 lines
2.9 KiB
TypeScript
/**
|
|
* MessageText — rendert Chat-Text mit Auto-Linkifizierung:
|
|
* - http(s)://... → tippbar, oeffnet im Browser
|
|
* - mailto: oder plain E-Mail → tippbar, oeffnet Mail-App
|
|
* - Telefonnummern → tippbar, oeffnet Android-Dialer
|
|
*
|
|
* Text ist durchgaengig markierbar/kopierbar (selectable).
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { Text, Linking, TextStyle, StyleProp } from 'react-native';
|
|
|
|
// Regex kombiniert URL | Email | Telefonnummer.
|
|
// Gruppenreihenfolge ist wichtig fuer die Erkennung unten.
|
|
//
|
|
// URL: http://... oder https://... bis zum ersten Whitespace / Anfuehrungszeichen.
|
|
// Email: simpler Standard-Match (kein RFC-kompatibel aber gut genug).
|
|
// Telefon: internationale Form (+49..., 0049..., 0176...), darf Leerzeichen
|
|
// / Bindestriche / Schraegstriche / Klammern enthalten, mindestens 7
|
|
// Ziffern insgesamt. Vermeidet banale Zahlen (Uhrzeiten, Datum).
|
|
const LINK_REGEX = new RegExp(
|
|
'(https?:\\/\\/[^\\s<>"]+)' + // 1: URL
|
|
'|([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})' + // 2: Email
|
|
'|((?:\\+|00)\\d[\\d\\s()\\-\\/]{6,}\\d|0\\d{2,4}[\\s\\/\\-]?[\\d\\s\\-\\/]{5,}\\d)', // 3: Telefon
|
|
'g',
|
|
);
|
|
|
|
const LINK_STYLE = { color: '#0096FF', textDecorationLine: 'underline' } as TextStyle;
|
|
|
|
interface Segment {
|
|
text: string;
|
|
kind: 'text' | 'url' | 'email' | 'phone';
|
|
}
|
|
|
|
function tokenize(raw: string): Segment[] {
|
|
const out: Segment[] = [];
|
|
let lastEnd = 0;
|
|
LINK_REGEX.lastIndex = 0;
|
|
let m: RegExpExecArray | null;
|
|
while ((m = LINK_REGEX.exec(raw)) !== null) {
|
|
if (m.index > lastEnd) {
|
|
out.push({ text: raw.slice(lastEnd, m.index), kind: 'text' });
|
|
}
|
|
if (m[1]) out.push({ text: m[1], kind: 'url' });
|
|
else if (m[2]) out.push({ text: m[2], kind: 'email' });
|
|
else if (m[3]) out.push({ text: m[3], kind: 'phone' });
|
|
lastEnd = LINK_REGEX.lastIndex;
|
|
}
|
|
if (lastEnd < raw.length) out.push({ text: raw.slice(lastEnd), kind: 'text' });
|
|
return out;
|
|
}
|
|
|
|
function onPress(seg: Segment) {
|
|
try {
|
|
if (seg.kind === 'url') {
|
|
Linking.openURL(seg.text);
|
|
} else if (seg.kind === 'email') {
|
|
Linking.openURL(`mailto:${seg.text}`);
|
|
} else if (seg.kind === 'phone') {
|
|
// Android-Dialer erwartet tel:-Schema ohne Leerzeichen/Bindestriche
|
|
const clean = seg.text.replace(/[\s\-\/()]/g, '');
|
|
Linking.openURL(`tel:${clean}`);
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
interface Props {
|
|
text: string;
|
|
style?: StyleProp<TextStyle>;
|
|
}
|
|
|
|
const MessageText: React.FC<Props> = ({ text, style }) => {
|
|
const segments = React.useMemo(() => tokenize(text), [text]);
|
|
return (
|
|
<Text style={style} selectable>
|
|
{segments.map((seg, i) => {
|
|
if (seg.kind === 'text') {
|
|
return <Text key={i}>{seg.text}</Text>;
|
|
}
|
|
return (
|
|
<Text key={i} style={LINK_STYLE} onPress={() => onPress(seg)}>
|
|
{seg.text}
|
|
</Text>
|
|
);
|
|
})}
|
|
</Text>
|
|
);
|
|
};
|
|
|
|
export default MessageText;
|