Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a66752655 | |||
| b510ccd93a | |||
| bbd51406a9 | |||
| 2cd436f6e9 |
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 10501
|
versionCode 10503
|
||||||
versionName "0.1.5.1"
|
versionName "0.1.5.3"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.5.1",
|
"version": "0.1.5.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -1381,15 +1381,22 @@ const ChatScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
const invertedMessages = useMemo(() => [...chatVisibleMessages].reverse(), [chatVisibleMessages]);
|
const invertedMessages = useMemo(() => [...chatVisibleMessages].reverse(), [chatVisibleMessages]);
|
||||||
|
|
||||||
// Such-Treffer: alle Message-IDs die zur Query passen, in chronologischer
|
// Such-Treffer: alle Message-IDs die zur Query passen. NEUESTE ZUERST —
|
||||||
// Reihenfolge (aelteste zuerst). Bei Query-Change resetten wir den Index.
|
// analog zu WhatsApp/Telegram: User ist visuell unten im Chat, der erste
|
||||||
|
// Treffer ist meist schon im Viewport (kein weiter Pre-Scroll, kein
|
||||||
|
// Cold-Start-Sprung-Fail). „Naechster" geht in die Vergangenheit.
|
||||||
|
// WICHTIG: nur in chatVisibleMessages suchen — Spezial-Bubbles (Memory/
|
||||||
|
// Skill/Trigger) sind im Chat-Stream nicht sichtbar und Treffer auf die
|
||||||
|
// wuerden zu „ID nicht im FlatList → findIndex=-1 → kein Scroll"-Fail
|
||||||
|
// fuehren.
|
||||||
const searchMatchIds = useMemo(() => {
|
const searchMatchIds = useMemo(() => {
|
||||||
const q = searchQuery.trim().toLowerCase();
|
const q = searchQuery.trim().toLowerCase();
|
||||||
if (!q) return [] as string[];
|
if (!q) return [] as string[];
|
||||||
return messages
|
return chatVisibleMessages
|
||||||
.filter(m => (m.text || '').toLowerCase().includes(q))
|
.filter(m => (m.text || '').toLowerCase().includes(q))
|
||||||
.map(m => m.id);
|
.map(m => m.id)
|
||||||
}, [messages, searchQuery]);
|
.reverse();
|
||||||
|
}, [chatVisibleMessages, searchQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSearchIndex(0);
|
setSearchIndex(0);
|
||||||
@@ -1425,11 +1432,11 @@ const ChatScreen: React.FC = () => {
|
|||||||
// Den aktuellen Snapshot von invertedMessages holen wir via Ref.
|
// Den aktuellen Snapshot von invertedMessages holen wir via Ref.
|
||||||
const invertedMessagesRef = useRef(invertedMessages);
|
const invertedMessagesRef = useRef(invertedMessages);
|
||||||
invertedMessagesRef.current = invertedMessages;
|
invertedMessagesRef.current = invertedMessages;
|
||||||
// Schaetzwert fuer durchschnittliche Bubble-Hoehe. Wird fuer den
|
// Cache fuer echte Bubble-Hoehen, gefuettert per onLayout in
|
||||||
// ersten Pre-Scroll genutzt wenn FlatList noch keine Layouts hat.
|
// renderMessage. Wird beim Pre-Scroll genutzt damit der grobe Sprung
|
||||||
// 150 px ist Mittel zwischen kurzen Voice-Bubbles (~70) und langen
|
// praezise landet (statt mit dem 150-px-Pauschalwert weit daneben).
|
||||||
// ARIA-Antworten (~250).
|
const itemHeights = useRef<Map<string, number>>(new Map());
|
||||||
const AVG_BUBBLE_HEIGHT = 150;
|
const AVG_BUBBLE_HEIGHT = 150; // Fallback fuer noch nicht gemessene Items
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchMatchIds.length) {
|
if (!searchMatchIds.length) {
|
||||||
lastSearchScrollKey.current = '';
|
lastSearchScrollKey.current = '';
|
||||||
@@ -1447,19 +1454,28 @@ const ChatScreen: React.FC = () => {
|
|||||||
clearPendingScrollRetry();
|
clearPendingScrollRetry();
|
||||||
const idx = invertedMessagesRef.current.findIndex(m => m.id === id);
|
const idx = invertedMessagesRef.current.findIndex(m => m.id === id);
|
||||||
if (idx < 0 || !flatListRef.current) return;
|
if (idx < 0 || !flatListRef.current) return;
|
||||||
// Pre-Scroll: erst grob mit konstanter Hoehe in die Naehe springen.
|
// Pre-Scroll: erst grob in die Naehe springen, damit FlatList die
|
||||||
// Damit hat FlatList ueberhaupt die Chance die Layouts der Bubbles in
|
// Bubbles in der Umgebung ueberhaupt rendert (sonst basiert
|
||||||
// der Naehe zu rendern — sonst basiert info.averageItemLength im
|
// averageItemLength im Failed-Handler nur auf den ersten ~10 Items
|
||||||
// Failed-Handler nur auf den ersten 10 Items (Default initialNumToRender)
|
// und liefert einen voellig falschen Sprung).
|
||||||
// und liefert beim ersten Such-Versuch nach App-Start einen voellig
|
// Offset = Summe echter Hoehen (aus itemHeights-Cache, gefuettert per
|
||||||
// falschen Sprung.
|
// onLayout) + Fallback AVG fuer noch nicht gemessene. Bei „cold start"
|
||||||
|
// ist der Cache leer → AVG fuer alle → grob. Beim zweiten Such-Versuch
|
||||||
|
// sind die Bubbles in der Naehe gemessen → genauer.
|
||||||
|
let preOffset = 0;
|
||||||
|
const inv = invertedMessagesRef.current;
|
||||||
|
for (let i = 0; i < idx; i++) {
|
||||||
|
preOffset += itemHeights.current.get(inv[i].id) || AVG_BUBBLE_HEIGHT;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
flatListRef.current?.scrollToOffset({
|
flatListRef.current?.scrollToOffset({
|
||||||
offset: idx * AVG_BUBBLE_HEIGHT,
|
offset: preOffset,
|
||||||
animated: false,
|
animated: false,
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
// Nach kurzer Render-Pause praezise nachsetzen
|
// Nach kurzer Render-Pause praezise nachsetzen. 200 ms statt 80 ms —
|
||||||
|
// bei Cold-Start braucht FlatList laenger fuer das Item-Layout, das
|
||||||
|
// war Stefans „erst beim zweiten Versuch klappt's"-Bug.
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
@@ -1467,7 +1483,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
} catch {
|
} catch {
|
||||||
// onScrollToIndexFailed-Handler uebernimmt den Fallback
|
// onScrollToIndexFailed-Handler uebernimmt den Fallback
|
||||||
}
|
}
|
||||||
}, 80);
|
}, 200);
|
||||||
});
|
});
|
||||||
}, [searchIndex, searchMatchIds]);
|
}, [searchIndex, searchMatchIds]);
|
||||||
|
|
||||||
@@ -1885,7 +1901,15 @@ const ChatScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.messageBubble, isUser ? styles.userBubble : styles.ariaBubble, searchHighlightStyle]}>
|
<View
|
||||||
|
style={[styles.messageBubble, isUser ? styles.userBubble : styles.ariaBubble, searchHighlightStyle]}
|
||||||
|
onLayout={e => {
|
||||||
|
// Echte Hoehe in Cache speichern — Pre-Scroll der Suche nutzt
|
||||||
|
// die summierten Cache-Werte fuer praezisen Sprung. Bei
|
||||||
|
// unbekannten Items faellt's auf AVG_BUBBLE_HEIGHT zurueck.
|
||||||
|
itemHeights.current.set(item.id, e.nativeEvent.layout.height);
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* Anhang-Vorschau */}
|
{/* Anhang-Vorschau */}
|
||||||
{item.attachments?.map((att, idx) => (
|
{item.attachments?.map((att, idx) => (
|
||||||
<View key={idx}>
|
<View key={idx}>
|
||||||
|
|||||||
@@ -1798,7 +1798,7 @@ const SettingsScreen: React.FC = () => {
|
|||||||
<Text style={styles.aboutTitle}>ARIA Cockpit</Text>
|
<Text style={styles.aboutTitle}>ARIA Cockpit</Text>
|
||||||
<Text style={styles.aboutVersion}>Version {require('../../package.json').version}</Text>
|
<Text style={styles.aboutVersion}>Version {require('../../package.json').version}</Text>
|
||||||
<Text style={styles.aboutInfo}>
|
<Text style={styles.aboutInfo}>
|
||||||
ARIA \u2014 Autonomous Reasoning & Intelligence Assistant.{'\n'}
|
ARIA {'\u2014'} Autonomous Reasoning & Intelligence Assistant.{'\n'}
|
||||||
Stefans Kommandozentrale.{'\n'}
|
Stefans Kommandozentrale.{'\n'}
|
||||||
Gebaut mit React Native + TypeScript.
|
Gebaut mit React Native + TypeScript.
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ class GpsTrackingService {
|
|||||||
private listeners: Set<Listener> = new Set();
|
private listeners: Set<Listener> = new Set();
|
||||||
// Defensive: nicht zu schnell oeffentlich togglen
|
// Defensive: nicht zu schnell oeffentlich togglen
|
||||||
private lastChangeAt = 0;
|
private lastChangeAt = 0;
|
||||||
|
// Letzte bekannte Position — wird vom Heartbeat-Timer alle 60s erneut
|
||||||
|
// an die Bridge gesendet, sonst veraltet near() im Brain (NEAR_MAX_AGE_SEC
|
||||||
|
// = 5 min) wenn der User stationaer ist und distanceFilter keine Updates
|
||||||
|
// mehr triggert.
|
||||||
|
private lastLat: number | null = null;
|
||||||
|
private lastLon: number | null = null;
|
||||||
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
isActive(): boolean {
|
isActive(): boolean {
|
||||||
return this.active;
|
return this.active;
|
||||||
@@ -84,6 +91,8 @@ class GpsTrackingService {
|
|||||||
(pos) => {
|
(pos) => {
|
||||||
const lat = pos.coords.latitude;
|
const lat = pos.coords.latitude;
|
||||||
const lon = pos.coords.longitude;
|
const lon = pos.coords.longitude;
|
||||||
|
this.lastLat = lat;
|
||||||
|
this.lastLon = lon;
|
||||||
rvs.send('location_update' as any, { lat, lon });
|
rvs.send('location_update' as any, { lat, lon });
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
@@ -96,6 +105,17 @@ class GpsTrackingService {
|
|||||||
fastestInterval: 10000, // (Android) max Frequenz
|
fastestInterval: 10000, // (Android) max Frequenz
|
||||||
} as any,
|
} as any,
|
||||||
);
|
);
|
||||||
|
// Heartbeat: alle 60s die letzte bekannte Position erneut senden.
|
||||||
|
// Sonst bleibt der Brain-State stale wenn der User stationaer ist
|
||||||
|
// (distanceFilter blockt watchPosition-Updates) → near()-Watcher
|
||||||
|
// verwerfen die Position als veraltet (NEAR_MAX_AGE_SEC = 300s).
|
||||||
|
// Kein neuer GPS-Wakeup, nur Re-Send der letzten Werte → akkufreundlich.
|
||||||
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
if (this.lastLat != null && this.lastLon != null) {
|
||||||
|
rvs.send('location_update' as any, { lat: this.lastLat, lon: this.lastLon });
|
||||||
|
}
|
||||||
|
}, 60_000);
|
||||||
this.active = true;
|
this.active = true;
|
||||||
this.lastChangeAt = Date.now();
|
this.lastChangeAt = Date.now();
|
||||||
this.notify();
|
this.notify();
|
||||||
@@ -118,6 +138,10 @@ class GpsTrackingService {
|
|||||||
try { Geolocation.clearWatch(this.watchId); } catch {}
|
try { Geolocation.clearWatch(this.watchId); } catch {}
|
||||||
this.watchId = null;
|
this.watchId = null;
|
||||||
}
|
}
|
||||||
|
if (this.heartbeatTimer) {
|
||||||
|
clearInterval(this.heartbeatTimer);
|
||||||
|
this.heartbeatTimer = null;
|
||||||
|
}
|
||||||
this.active = false;
|
this.active = false;
|
||||||
this.lastChangeAt = Date.now();
|
this.lastChangeAt = Date.now();
|
||||||
this.notify();
|
this.notify();
|
||||||
|
|||||||
Reference in New Issue
Block a user