fix+feat(projects): Spinner-Bug, Back-Button, kollabierbare Chat-Bloecke, File-Filter
Drei Stefan-Bugs aus dem ersten Deploy-Test plus die fehlenden Polish-
Features fuer die Projekt-Funktion.
Fixes:
- ProjectsBrowser-Spinner-Hang: useRef-Pattern statt useCallback([onActive
Changed]) — Parent uebergibt inline-arrow-Callbacks, neue Identitaet
jedes Render → useCallback recomputes → useEffect refeuert → infinite
Spinner. Fix: Ref-Bridge fuer Callbacks, useCallback mit empty deps.
- ChatScreen Banner: zusaetzlicher × Hauptchat-Button rechts (sichtbar
nur wenn Projekt aktiv) — ein Tap und zurueck zum Hauptthread, ohne
Modal-Umweg.
Features:
- Brain ChatOut.project_id: aktive Projekt-ID NACH dem Turn (kann
durch project_enter/exit-Tools waehrend Turn gewechselt sein). Bridge
liest sie aus dem /chat-Response und haengt sie an jeden ARIA-Chat-
Broadcast als payload.projectId.
- App: ChatMessage.projectId-Feld. User-Bubbles werden mit aktiver
Projekt-ID getaggt vor dem Senden (auch im RVS-Payload). ARIA-Bubbles
kriegen die ID vom Bridge.
- App: Chat-Verlauf rendert aufeinanderfolgende Project-Messages als
einklappbaren Block mit Header (▶/▼ + Projekt-Name + Count). Auto-
Collapse beim Projekt-Wechsel (altes ein, neues aus), Default beim
ersten Render: alle inaktiven Projekte eingeklappt.
- File-Manager Project-Tagging:
- diagnostic/server.js: Manifest /shared/config/file_projects.json
+ /api/files-list returnt projectId pro Datei + neuer Endpoint
/api/files-set-project.
- bridge/aria_bridge.py: nach App-Upload Auto-Tag mit aktivem Projekt
(Brain-Status-Query, best-effort fail-silent).
- App SettingsScreen: scrollbare Projekt-Pill-Reihe als Filter, default
auf aktives Projekt wenn vorhanden, sonst "Alle Projekte".
- Diagnostic: zweites Dropdown im Files-Tab, baut Projekt-Optionen
dynamisch aus /api/brain/projects/list.
Bewusst nicht drin (Folgeschritt):
- Per-File "Projekt zuweisen"-Action (Long-Press / Right-Click)
- Filter-Sync zwischen ChatScreen-Banner und SettingsScreen-Filter
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
* SettingsScreen.tsx in der Section 'projects'.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
@@ -60,17 +60,23 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editDesc, setEditDesc] = useState('');
|
||||
|
||||
// Refs damit useCallback NICHT bei jeder Re-Render des Parents neu erzeugt
|
||||
// wird (parent uebergibt oft inline-arrow-Callbacks, neue Identity jedes
|
||||
// Render → useCallback re-runs → useEffect refeuert → infinite spinner).
|
||||
const onActiveChangedRef = useRef(onActiveChanged);
|
||||
useEffect(() => { onActiveChangedRef.current = onActiveChanged; }, [onActiveChanged]);
|
||||
|
||||
const load = useCallback(() => {
|
||||
setLoading(true); setErr(null);
|
||||
brainApi.getProjectStatus()
|
||||
.then(status => {
|
||||
setProjects(status.projects || []);
|
||||
setActiveId(status.active_id || '');
|
||||
onActiveChanged?.(status.active);
|
||||
onActiveChangedRef.current?.(status.active);
|
||||
})
|
||||
.catch(e => setErr(String(e?.message || e)))
|
||||
.finally(() => setLoading(false));
|
||||
}, [onActiveChanged]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => { if (visible) load(); }, [visible, load]);
|
||||
|
||||
@@ -85,10 +91,10 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
|
||||
brainApi.switchProject(id)
|
||||
.then(status => {
|
||||
setActiveId(status.active_id || '');
|
||||
onActiveChanged?.(status.active);
|
||||
onActiveChangedRef.current?.(status.active);
|
||||
})
|
||||
.catch(e => Alert.alert('Fehler', String(e?.message || e)));
|
||||
}, [onActiveChanged]);
|
||||
}, []);
|
||||
|
||||
const createProject = useCallback(() => {
|
||||
const name = newName.trim();
|
||||
|
||||
@@ -70,6 +70,9 @@ interface ChatMessage {
|
||||
text: string;
|
||||
timestamp: number;
|
||||
attachments?: Attachment[];
|
||||
/** Projekt-Zuordnung — leer = Hauptchat. Wird genutzt um Bubbles zu
|
||||
* Projekt-Bloecken zu gruppieren (auf/einklappbar). */
|
||||
projectId?: string;
|
||||
/** Bridge-Message-ID zur Zuordnung von TTS-Audio */
|
||||
messageId?: string;
|
||||
/** Lokaler Pfad zur gecachten TTS-Audio-Datei (file://...) */
|
||||
@@ -284,6 +287,8 @@ const ChatScreen: React.FC = () => {
|
||||
const [searchVisible, setSearchVisible] = useState(false);
|
||||
const [projectsVisible, setProjectsVisible] = useState(false);
|
||||
const [activeProject, setActiveProject] = useState<BrainProject | null>(null);
|
||||
// Lookup-Map id → Projekt — fuer Header-Names im Chat-Verlauf
|
||||
const [projectNameById, setProjectNameById] = useState<Record<string, string>>({});
|
||||
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
|
||||
const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]);
|
||||
const [agentActivity, setAgentActivity] = useState<{activity: string, tool: string}>({activity: 'idle', tool: ''});
|
||||
@@ -466,7 +471,14 @@ const ChatScreen: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const loadProject = () => {
|
||||
brainApi.getProjectStatus()
|
||||
.then(s => setActiveProject(s.active || null))
|
||||
.then(s => {
|
||||
setActiveProject(s.active || null);
|
||||
// Lookup-Map fuellen damit der Chat-Verlauf Header mit Namen rendern kann
|
||||
const map: Record<string, string> = {};
|
||||
for (const p of (s.projects || [])) map[p.id] = p.name;
|
||||
if (s.active) map[s.active.id] = s.active.name;
|
||||
setProjectNameById(prev => ({ ...prev, ...map }));
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
loadProject();
|
||||
@@ -1072,6 +1084,7 @@ const ChatScreen: React.FC = () => {
|
||||
attachments: message.payload.attachments as Attachment[] | undefined,
|
||||
messageId: (message.payload.messageId as string) || undefined,
|
||||
backupTs: (message.payload.backupTs as number) || undefined,
|
||||
projectId: ((message.payload as any).projectId as string) || '',
|
||||
};
|
||||
// ARIA hat geantwortet → alle User-Bubbles davor als 'delivered'
|
||||
// markieren (WhatsApp-Doppelhaken ✓✓). Brain hat sie verarbeitet.
|
||||
@@ -1614,7 +1627,87 @@ const ChatScreen: React.FC = () => {
|
||||
() => messages.filter(m => !m.memorySaved && !m.triggerCreated && !m.skillCreated),
|
||||
[messages],
|
||||
);
|
||||
const invertedMessages = useMemo(() => [...chatVisibleMessages].reverse(), [chatVisibleMessages]);
|
||||
|
||||
// Projekt-Bloecke: aufeinanderfolgende Nachrichten mit gleicher projectId
|
||||
// werden visuell gruppiert. Erstes Message-Item einer Gruppe bekommt einen
|
||||
// Header darueber (tappable für collapse). Collapsed → restliche Messages
|
||||
// der Gruppe werden ausgefiltert; nur Header bleibt sichtbar.
|
||||
// Hauptchat-Nachrichten (projectId leer) bleiben ungruppiert dazwischen.
|
||||
const [collapsedProjects, setCollapsedProjects] = useState<Set<string>>(new Set());
|
||||
|
||||
const projectMeta = useMemo(() => {
|
||||
// Pre-compute: welche Message ist Erst-Element ihrer Projekt-Gruppe?
|
||||
// Plus: wieviele Messages pro Projekt insgesamt (fuer Header-Count).
|
||||
const firstOfGroup = new Set<string>();
|
||||
const counts = new Map<string, number>();
|
||||
let lastPid: string | null = null;
|
||||
for (const m of chatVisibleMessages) {
|
||||
const pid = m.projectId || '';
|
||||
if (pid) counts.set(pid, (counts.get(pid) || 0) + 1);
|
||||
if (pid && pid !== lastPid) firstOfGroup.add(m.id);
|
||||
lastPid = pid || null;
|
||||
}
|
||||
return { firstOfGroup, counts };
|
||||
}, [chatVisibleMessages]);
|
||||
|
||||
// Render-Filter: bei collapsed Projekten zeigen wir NUR das erste Message-
|
||||
// Item der Gruppe (das traegt den Header). Restliche Messages werden ausge-
|
||||
// blendet — Header allein steht dann zwischen Hauptchat-Bubbles.
|
||||
const messagesForRender = useMemo(() => {
|
||||
return chatVisibleMessages.filter(m => {
|
||||
const pid = m.projectId || '';
|
||||
if (!pid) return true;
|
||||
if (!collapsedProjects.has(pid)) return true;
|
||||
return projectMeta.firstOfGroup.has(m.id);
|
||||
});
|
||||
}, [chatVisibleMessages, collapsedProjects, projectMeta]);
|
||||
|
||||
// Auto-Collapse beim Projekt-Wechsel: altes Projekt einklappen, neues aufklappen.
|
||||
const prevActiveIdRef = useRef<string>('');
|
||||
useEffect(() => {
|
||||
const newActive = activeProject?.id || '';
|
||||
const prevActive = prevActiveIdRef.current;
|
||||
if (newActive === prevActive) return;
|
||||
setCollapsedProjects(prev => {
|
||||
const next = new Set(prev);
|
||||
if (prevActive) next.add(prevActive);
|
||||
if (newActive) next.delete(newActive);
|
||||
return next;
|
||||
});
|
||||
prevActiveIdRef.current = newActive;
|
||||
}, [activeProject]);
|
||||
|
||||
// Default: alle Projekte einklappen außer dem aktiven (Stefan-Vision:
|
||||
// „beim ersten Öffnen sind alle projekte eingeklappt").
|
||||
const collapseInitialized = useRef(false);
|
||||
useEffect(() => {
|
||||
if (collapseInitialized.current) return;
|
||||
if (chatVisibleMessages.length === 0) return;
|
||||
const allPids = new Set<string>();
|
||||
for (const m of chatVisibleMessages) {
|
||||
if (m.projectId) allPids.add(m.projectId);
|
||||
}
|
||||
if (allPids.size === 0) return;
|
||||
const activePid = activeProject?.id || '';
|
||||
setCollapsedProjects(prev => {
|
||||
const next = new Set(prev);
|
||||
for (const pid of allPids) {
|
||||
if (pid !== activePid) next.add(pid);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
collapseInitialized.current = true;
|
||||
}, [chatVisibleMessages, activeProject]);
|
||||
|
||||
const toggleProjectCollapse = useCallback((projectId: string) => {
|
||||
setCollapsedProjects(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(projectId)) next.delete(projectId); else next.add(projectId);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const invertedMessages = useMemo(() => [...messagesForRender].reverse(), [messagesForRender]);
|
||||
|
||||
// Such-Treffer: alle Message-IDs die zur Query passen. NEUESTE ZUERST —
|
||||
// analog zu WhatsApp/Telegram: User ist visuell unten im Chat, der erste
|
||||
@@ -1816,6 +1909,7 @@ const ChatScreen: React.FC = () => {
|
||||
const location = await getCurrentLocation();
|
||||
|
||||
const cmid = nextClientMsgId();
|
||||
const activePid = activeProject?.id || '';
|
||||
const userMsg: ChatMessage = {
|
||||
id: nextId(),
|
||||
sender: 'user',
|
||||
@@ -1824,16 +1918,18 @@ const ChatScreen: React.FC = () => {
|
||||
clientMsgId: cmid,
|
||||
deliveryStatus: connectionStateRef.current === 'connected' ? 'sending' : 'queued',
|
||||
sendAttempts: 1,
|
||||
projectId: activePid,
|
||||
};
|
||||
setMessages(prev => capMessages([...prev, userMsg]));
|
||||
|
||||
console.log('[Chat] sende cmid=%s voice=%s speed=%s interrupted=%s',
|
||||
cmid, localXttsVoiceRef.current || '(default)', ttsSpeedRef.current, wasInterrupted);
|
||||
console.log('[Chat] sende cmid=%s voice=%s speed=%s interrupted=%s project=%s',
|
||||
cmid, localXttsVoiceRef.current || '(default)', ttsSpeedRef.current, wasInterrupted, activePid || '(main)');
|
||||
dispatchWithAck(cmid, 'chat', {
|
||||
text,
|
||||
voice: localXttsVoiceRef.current,
|
||||
speed: ttsSpeedRef.current,
|
||||
interrupted: wasInterrupted,
|
||||
projectId: activePid,
|
||||
...(location && { location }),
|
||||
});
|
||||
}, [inputText, getCurrentLocation, pendingAttachments, sendPendingAttachments, interruptAriaIfBusy, dispatchWithAck]);
|
||||
@@ -2332,6 +2428,44 @@ const ChatScreen: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Wrapper: setzt einen einklappbaren Projekt-Header VOR die Bubble wenn
|
||||
// diese das erste Element ihrer Projekt-Gruppe ist. Bei collapsed
|
||||
// → nur Header, keine Bubble. Hauptchat-Nachrichten gehen ohne Header durch.
|
||||
const renderMessageWithProjectHeader = ({ item }: { item: ChatMessage }) => {
|
||||
const pid = item.projectId || '';
|
||||
if (!pid) return renderMessage({ item });
|
||||
const isFirstOfGroup = projectMeta.firstOfGroup.has(item.id);
|
||||
if (!isFirstOfGroup) return renderMessage({ item });
|
||||
const isCollapsed = collapsedProjects.has(pid);
|
||||
const projectName = projectNameById[pid] || pid;
|
||||
const projectCount = projectMeta.counts.get(pid) || 0;
|
||||
const header = (
|
||||
<TouchableOpacity
|
||||
onPress={() => toggleProjectCollapse(pid)}
|
||||
style={{
|
||||
marginVertical: 6, marginHorizontal: 8, paddingHorizontal: 10, paddingVertical: 8,
|
||||
borderRadius: 6, backgroundColor: 'rgba(52,199,89,0.10)',
|
||||
borderLeftWidth: 3, borderLeftColor: '#34C759',
|
||||
flexDirection: 'row', alignItems: 'center', gap: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 14, color: '#34C759', fontWeight: '700', flex: 1 }} numberOfLines={1}>
|
||||
{isCollapsed ? '▶' : '▼'} 📁 {projectName}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 11, color: '#8888AA' }}>
|
||||
{projectCount} {projectCount === 1 ? 'Nachricht' : 'Nachrichten'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
if (isCollapsed) return header;
|
||||
return (
|
||||
<View>
|
||||
{header}
|
||||
{renderMessage({ item })}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// Extrahiert kopierbare Items aus dem Bubble-Text (URLs, Mails, Telefon).
|
||||
// Wird vom Long-Press/Copy-Menu genutzt damit Stefan den einzelnen Wert
|
||||
// teilen kann ohne den umliegenden Text mitzunehmen.
|
||||
@@ -2483,9 +2617,8 @@ const ChatScreen: React.FC = () => {
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Projekt-Indicator: zeigt Hauptchat oder aktives Projekt, Tap öffnet Liste */}
|
||||
<TouchableOpacity
|
||||
onPress={() => setProjectsVisible(true)}
|
||||
{/* Projekt-Indicator: zeigt Hauptchat oder aktives Projekt */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row', alignItems: 'center',
|
||||
paddingHorizontal: 12, paddingVertical: 6,
|
||||
@@ -2494,13 +2627,26 @@ const ChatScreen: React.FC = () => {
|
||||
borderColor: activeProject ? '#34C759' : '#1E1E2E',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 13, color: activeProject ? '#34C759' : '#8888AA', fontWeight: activeProject ? '700' : '500', flex: 1 }} numberOfLines={1}>
|
||||
{activeProject ? `📁 ${activeProject.name}` : '💬 Hauptchat'}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 11, color: '#555570' }}>
|
||||
{activeProject ? 'wechseln ›' : 'Projekte ›'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setProjectsVisible(true)} style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ fontSize: 13, color: activeProject ? '#34C759' : '#8888AA', fontWeight: activeProject ? '700' : '500', flex: 1 }} numberOfLines={1}>
|
||||
{activeProject ? `📁 ${activeProject.name}` : '💬 Hauptchat'}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 11, color: '#555570', marginRight: 8 }}>
|
||||
{activeProject ? 'wechseln ›' : 'Projekte ›'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{activeProject && (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
brainApi.switchProject('').then(s => setActiveProject(s.active || null)).catch(() => {});
|
||||
}}
|
||||
style={{ paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4, backgroundColor: 'rgba(255,255,255,0.08)' }}
|
||||
hitSlop={{top:8,bottom:8,left:8,right:8}}
|
||||
>
|
||||
<Text style={{ fontSize: 11, color: '#E0E0F0', fontWeight: '600' }}>× Hauptchat</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Projekt-Modal */}
|
||||
<ProjectsBrowser
|
||||
@@ -2591,7 +2737,7 @@ const ChatScreen: React.FC = () => {
|
||||
}, 300);
|
||||
}}
|
||||
keyExtractor={item => item.id}
|
||||
renderItem={renderMessage}
|
||||
renderItem={renderMessageWithProjectHeader}
|
||||
contentContainerStyle={styles.messageList}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={
|
||||
|
||||
@@ -93,6 +93,7 @@ import SkillBrowser from '../components/SkillBrowser';
|
||||
import OAuthBrowser from '../components/OAuthBrowser';
|
||||
import VoiceIdEnrollment from '../components/VoiceIdEnrollment';
|
||||
import ProjectsBrowser from '../components/ProjectsBrowser';
|
||||
import brainApi from '../services/brainApi';
|
||||
import { isVerboseLogging, setVerboseLogging, isDebugLogsToBridge, setDebugLogsToBridge, APP_LOG_EVENT } from '../services/logger';
|
||||
import {
|
||||
isWakeReadySoundEnabled,
|
||||
@@ -204,7 +205,9 @@ const SettingsScreen: React.FC = () => {
|
||||
const [availableVoices, setAvailableVoices] = useState<Array<{name: string, size: number}>>([]);
|
||||
// Datei-Manager
|
||||
const [fileManagerOpen, setFileManagerOpen] = useState(false);
|
||||
const [fileManagerFiles, setFileManagerFiles] = useState<Array<{name: string; path: string; size: number; mtime: number; fromAria: boolean}>>([]);
|
||||
const [fileManagerFiles, setFileManagerFiles] = useState<Array<{name: string; path: string; size: number; mtime: number; fromAria: boolean; projectId?: string}>>([]);
|
||||
const [fileFilterProjectId, setFileFilterProjectId] = useState<string>('__all__');
|
||||
const [fileFilterProjects, setFileFilterProjects] = useState<Array<{id: string; name: string}>>([]);
|
||||
const [fileManagerLoading, setFileManagerLoading] = useState(false);
|
||||
const [fileManagerError, setFileManagerError] = useState('');
|
||||
const [fileManagerSearch, setFileManagerSearch] = useState('');
|
||||
@@ -726,6 +729,20 @@ const SettingsScreen: React.FC = () => {
|
||||
return () => unsub();
|
||||
}, [fileManagerOpen]);
|
||||
|
||||
// Beim Oeffnen des Datei-Managers: Projekt-Liste laden fuer den Filter.
|
||||
useEffect(() => {
|
||||
if (!fileManagerOpen) return;
|
||||
brainApi.listProjects(true)
|
||||
.then(list => setFileFilterProjects(list.map(p => ({ id: p.id, name: p.name }))))
|
||||
.catch(() => {});
|
||||
// Default-Filter: aktives Projekt (falls vorhanden), sonst "alle".
|
||||
brainApi.getProjectStatus()
|
||||
.then(s => {
|
||||
if (s.active_id) setFileFilterProjectId(s.active_id);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [fileManagerOpen]);
|
||||
|
||||
// --- QR-Code scannen ---
|
||||
|
||||
const openQRScanner = useCallback(() => {
|
||||
@@ -962,6 +979,29 @@ const SettingsScreen: React.FC = () => {
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
{/* Projekt-Filter: scrollbare Pill-Reihe. „Alle Projekte" + „Hauptchat" +
|
||||
ein Pill pro Projekt. Default = aktives Projekt (siehe useEffect oben). */}
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}
|
||||
style={{marginTop:6}} contentContainerStyle={{gap:6, paddingRight:8}}>
|
||||
{[
|
||||
{ id: '__all__', name: '📁 Alle Projekte' },
|
||||
{ id: '', name: '💬 Hauptchat' },
|
||||
...fileFilterProjects,
|
||||
].map(p => (
|
||||
<TouchableOpacity
|
||||
key={p.id || 'mainchat'}
|
||||
onPress={() => setFileFilterProjectId(p.id)}
|
||||
style={{
|
||||
paddingVertical:6, paddingHorizontal:12, borderRadius:14,
|
||||
backgroundColor: fileFilterProjectId === p.id ? '#34C759' : '#1E1E2E',
|
||||
}}
|
||||
>
|
||||
<Text style={{color: fileFilterProjectId === p.id ? '#fff' : '#8888AA', fontSize:12}}>
|
||||
{p.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
{fileManagerLoading ? (
|
||||
<Text style={{color:'#8888AA', textAlign:'center', marginTop:20}}>Lade...</Text>
|
||||
@@ -972,6 +1012,11 @@ const SettingsScreen: React.FC = () => {
|
||||
let files = fileManagerFiles;
|
||||
if (fileManagerFilter === 'aria') files = files.filter(f => f.fromAria);
|
||||
else if (fileManagerFilter === 'user') files = files.filter(f => !f.fromAria);
|
||||
// Projekt-Filter: '__all__' = alles, '' = Hauptchat (kein project_id),
|
||||
// sonst exakte project_id-Match.
|
||||
if (fileFilterProjectId !== '__all__') {
|
||||
files = files.filter(f => (f.projectId || '') === fileFilterProjectId);
|
||||
}
|
||||
if (fileManagerSearch) {
|
||||
const q = fileManagerSearch.toLowerCase();
|
||||
files = files.filter(f => f.name.toLowerCase().includes(q));
|
||||
|
||||
Reference in New Issue
Block a user