KI-AGENT: Ungelesene Chat Badges in Mobile App anzeigen
This commit is contained in:
@@ -1,14 +1,49 @@
|
|||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
import { Redirect, Tabs } from 'expo-router';
|
import { Redirect, Tabs } from 'expo-router';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { HapticTab } from '@/components/haptic-tab';
|
import { HapticTab } from '@/components/haptic-tab';
|
||||||
import { IconSymbol } from '@/components/ui/icon-symbol';
|
import { IconSymbol } from '@/components/ui/icon-symbol';
|
||||||
import { Colors } from '@/constants/theme';
|
import { Colors } from '@/constants/theme';
|
||||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
import { useColorScheme } from '@/hooks/use-color-scheme';
|
||||||
|
import { fetchMatrixUnreadCounts } from '@/src/lib/api';
|
||||||
import { useAuth } from '@/src/providers/auth-provider';
|
import { useAuth } from '@/src/providers/auth-provider';
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
const { isBootstrapping, token, requiresTenantSelection } = useAuth();
|
const { activeTenantId, isBootstrapping, token, requiresTenantSelection } = useAuth();
|
||||||
|
const [communicationUnread, setCommunicationUnread] = useState(0);
|
||||||
|
|
||||||
|
const refreshCommunicationUnread = useCallback(async () => {
|
||||||
|
if (!token || requiresTenantSelection) {
|
||||||
|
setCommunicationUnread(0);
|
||||||
|
await Notifications.setBadgeCountAsync(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const unread = await fetchMatrixUnreadCounts(token);
|
||||||
|
const total = Object.values(unread).reduce((sum, room) => sum + (room.count || 0), 0);
|
||||||
|
setCommunicationUnread(total);
|
||||||
|
await Notifications.setBadgeCountAsync(total);
|
||||||
|
} catch {
|
||||||
|
// Badge loading must not block tab navigation.
|
||||||
|
}
|
||||||
|
}, [requiresTenantSelection, token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refreshCommunicationUnread();
|
||||||
|
|
||||||
|
if (!token || requiresTenantSelection) return undefined;
|
||||||
|
|
||||||
|
const id = setInterval(() => void refreshCommunicationUnread(), 30000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [activeTenantId, refreshCommunicationUnread, requiresTenantSelection, token]);
|
||||||
|
|
||||||
|
const communicationBadge = useMemo(() => {
|
||||||
|
if (!communicationUnread) return undefined;
|
||||||
|
return communicationUnread > 99 ? '99+' : communicationUnread;
|
||||||
|
}, [communicationUnread]);
|
||||||
|
|
||||||
if (isBootstrapping) {
|
if (isBootstrapping) {
|
||||||
return null;
|
return null;
|
||||||
@@ -54,6 +89,13 @@ export default function TabLayout() {
|
|||||||
name="communication"
|
name="communication"
|
||||||
options={{
|
options={{
|
||||||
title: 'Kommunikation',
|
title: 'Kommunikation',
|
||||||
|
tabBarBadge: communicationBadge,
|
||||||
|
tabBarBadgeStyle: {
|
||||||
|
backgroundColor: '#ef4444',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '800',
|
||||||
|
},
|
||||||
tabBarIcon: ({ color }) => <IconSymbol size={24} name="message.fill" color={color} />,
|
tabBarIcon: ({ color }) => <IconSymbol size={24} name="message.fill" color={color} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -185,6 +185,12 @@ export default function CommunicationScreen() {
|
|||||||
return users.filter((user) => !existing.has(user.matrixUserId));
|
return users.filter((user) => !existing.has(user.matrixUserId));
|
||||||
}, [members, users]);
|
}, [members, users]);
|
||||||
|
|
||||||
|
const clearRoomUnread = useCallback((roomKey: string) => {
|
||||||
|
setRooms((current) =>
|
||||||
|
current.map((room) => (room.key === roomKey ? { ...room, unread: 0, mentions: 0 } : room))
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const loadRooms = useCallback(async () => {
|
const loadRooms = useCallback(async () => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
const [nextStatus, identity, nextRooms, nextUsers] = await Promise.all([
|
const [nextStatus, identity, nextRooms, nextUsers] = await Promise.all([
|
||||||
@@ -221,7 +227,10 @@ export default function CommunicationScreen() {
|
|||||||
setMembers(sync.members || nextMembers);
|
setMembers(sync.members || nextMembers);
|
||||||
setSyncSince(sync.nextBatch);
|
setSyncSince(sync.nextBatch);
|
||||||
const last = merged.at(-1);
|
const last = merged.at(-1);
|
||||||
if (last?.id) await markMatrixRoomRead(token, roomKeyToLoad, last.id);
|
if (last?.id) {
|
||||||
|
await markMatrixRoomRead(token, roomKeyToLoad, last.id);
|
||||||
|
clearRoomUnread(roomKeyToLoad);
|
||||||
|
}
|
||||||
requestAnimationFrame(() => messageListRef.current?.scrollToEnd({ animated: true }));
|
requestAnimationFrame(() => messageListRef.current?.scrollToEnd({ animated: true }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Kommunikation konnte nicht geladen werden.');
|
setError(err instanceof Error ? err.message : 'Kommunikation konnte nicht geladen werden.');
|
||||||
@@ -230,7 +239,7 @@ export default function CommunicationScreen() {
|
|||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[token]
|
[clearRoomUnread, token]
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshAll = useCallback(
|
const refreshAll = useCallback(
|
||||||
@@ -255,19 +264,25 @@ export default function CommunicationScreen() {
|
|||||||
if (!token || !activeRoom?.exists || !syncSince) return;
|
if (!token || !activeRoom?.exists || !syncSince) return;
|
||||||
try {
|
try {
|
||||||
const sync = await syncMatrixRoom(token, activeRoom.key, syncSince);
|
const sync = await syncMatrixRoom(token, activeRoom.key, syncSince);
|
||||||
|
const incomingMessages = sync.messages || [];
|
||||||
setSyncSince(sync.nextBatch || syncSince);
|
setSyncSince(sync.nextBatch || syncSince);
|
||||||
setMembers((current) => sync.members || current);
|
setMembers((current) => sync.members || current);
|
||||||
setMessages((current) => {
|
setMessages((current) => {
|
||||||
let next = mergeMessages(current, sync.messages || []);
|
let next = mergeMessages(current, incomingMessages);
|
||||||
next = applyReplacements(next, sync.replacements);
|
next = applyReplacements(next, sync.replacements);
|
||||||
next = applyReactions(next, sync.reactions);
|
next = applyReactions(next, sync.reactions);
|
||||||
next = applyRedactions(next, sync.redactions);
|
next = applyRedactions(next, sync.redactions);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
const last = incomingMessages.at(-1);
|
||||||
|
if (last?.id) {
|
||||||
|
await markMatrixRoomRead(token, activeRoom.key, last.id);
|
||||||
|
clearRoomUnread(activeRoom.key);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Polling errors are surfaced by manual refresh to avoid noisy chat usage.
|
// Polling errors are surfaced by manual refresh to avoid noisy chat usage.
|
||||||
}
|
}
|
||||||
}, [activeRoom, syncSince, token]);
|
}, [activeRoom, clearRoomUnread, syncSince, token]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refreshAll(true);
|
void refreshAll(true);
|
||||||
|
|||||||
@@ -292,6 +292,8 @@ export type MatrixSyncResponse = {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MatrixUnreadCounts = Record<string, { count?: number; mentions?: number }>;
|
||||||
|
|
||||||
function buildUrl(path: string): string {
|
function buildUrl(path: string): string {
|
||||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||||
return path;
|
return path;
|
||||||
@@ -408,6 +410,11 @@ export async function fetchMatrixRooms(token: string): Promise<MatrixRoom[]> {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchMatrixUnreadCounts(token: string): Promise<MatrixUnreadCounts> {
|
||||||
|
const response = await apiRequest<{ rooms?: MatrixUnreadCounts }>('/api/communication/matrix/unread', { token });
|
||||||
|
return response.rooms || {};
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchMatrixUsers(token: string): Promise<MatrixUser[]> {
|
export async function fetchMatrixUsers(token: string): Promise<MatrixUser[]> {
|
||||||
const response = await apiRequest<{ users?: MatrixUser[] }>('/api/communication/matrix/users', { token });
|
const response = await apiRequest<{ users?: MatrixUser[] }>('/api/communication/matrix/users', { token });
|
||||||
return response.users || [];
|
return response.users || [];
|
||||||
|
|||||||
Reference in New Issue
Block a user