feat: Bug-Runde + 5 App/Diagnostic-Features
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>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user