fix(webapp): persist motion alerts in activity feed

This commit is contained in:
2026-04-06 13:30:00 +01:00
parent 3e0635fec3
commit 798fffa2a1
3 changed files with 89 additions and 9 deletions

View File

@@ -79,5 +79,8 @@ export const api = {
listRecordings: () => request('/recordings/me/list'),
getRecordingDownloadUrl: (recordingId) => request(`/recordings/${recordingId}/download-url`),
listNotifications: () => request('/push-notifications/me')
},
pushNotifications: {
markRead: (notificationId) => request(`/push-notifications/${notificationId}/read`, { method: 'POST', body: JSON.stringify({}) })
}
};

View File

@@ -811,11 +811,52 @@ const getCameraLabel = (cameraDeviceId, cameraName) => {
return `Camera ${cameraDeviceId?.substring(0, 6) ?? 'Unknown'}`;
};
const mapNotificationDeliveryToMotionNotification = (delivery) => {
const payload = delivery?.payload ?? {};
const cameraDeviceId = typeof payload.cameraDeviceId === 'string' ? payload.cameraDeviceId : '';
const deliveryType = typeof delivery?.type === 'string' ? delivery.type : '';
if (deliveryType !== 'motion_detected') {
return null;
}
return {
id: delivery.id,
eventId: typeof payload.eventId === 'string' ? payload.eventId : null,
cameraDeviceId,
message: `${getCameraLabel(cameraDeviceId)} has detected movement`,
createdAt: delivery.sentAt || delivery.createdAt || new Date().toISOString(),
isRead: delivery.status === 'read'
};
};
const syncMotionNotificationsFromDeliveries = (deliveries) => {
const motionNotifications = (deliveries || [])
.map(mapNotificationDeliveryToMotionNotification)
.filter(Boolean)
.sort((left, right) => new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime());
setAppState({ motionNotifications });
};
const refreshMotionNotifications = async () => {
const { device } = getAppState();
if (!device || device.role !== 'client') {
setAppState({ motionNotifications: [] });
return [];
}
const result = await api.ops.listNotifications().catch(() => ({ notifications: [] }));
syncMotionNotificationsFromDeliveries(result.notifications);
return result.notifications || [];
};
const pushMotionNotification = (cameraDeviceId) => {
if (!cameraDeviceId) return;
const notification = {
id: makeId(),
eventId: null,
cameraDeviceId,
message: `${getCameraLabel(cameraDeviceId)} has detected movement`,
createdAt: new Date().toISOString(),
@@ -825,22 +866,38 @@ const pushMotionNotification = (cameraDeviceId) => {
patchAppState((state) => ({
motionNotifications: [notification, ...state.motionNotifications].slice(0, 50)
}));
void refreshMotionNotifications();
};
const markMotionNotificationRead = (notificationId) => {
const markMotionNotificationRead = async (notificationId) => {
patchAppState((state) => ({
motionNotifications: state.motionNotifications.map((notification) =>
notification.id === notificationId ? { ...notification, isRead: true } : notification
)
}));
try {
await api.pushNotifications.markRead(notificationId);
} catch (error) {
console.error('Failed to persist notification read state', error);
}
};
const markAllNotificationsRead = () => {
const markAllNotificationsRead = async () => {
const unreadIds = getAppState().motionNotifications.filter((notification) => !notification.isRead).map((notification) => notification.id);
if (unreadIds.length === 0) {
return;
}
patchAppState((state) => ({
motionNotifications: state.motionNotifications.map((notification) =>
notification.isRead ? notification : { ...notification, isRead: true }
)
}));
await Promise.allSettled(unreadIds.map((notificationId) => api.pushNotifications.markRead(notificationId)));
};
const openRecordingModal = (downloadUrl, title) => {
@@ -1697,10 +1754,11 @@ const pollClientData = async () => {
const { device } = getAppState();
if (!device || device.role !== 'client') return;
const [recs, links, deviceList] = await Promise.all([
const [recs, links, deviceList, notifications] = await Promise.all([
api.ops.listRecordings().catch(() => ({ recordings: [] })),
api.devices.listLinks().catch(() => ({ links: [] })),
api.devices.list().catch(() => ({ devices: [] }))
api.devices.list().catch(() => ({ devices: [] })),
api.ops.listNotifications().catch(() => ({ notifications: [] }))
]);
const cameraById = new Map(
@@ -1722,6 +1780,7 @@ const pollClientData = async () => {
recordings: recs.recordings || [],
linkedCameras
});
syncMotionNotificationsFromDeliveries(notifications.notifications);
};
const startPolling = () => {
@@ -2194,7 +2253,7 @@ const actions = {
},
async openMotionNotificationTarget(notificationId, cameraDeviceId) {
markMotionNotificationRead(notificationId);
await markMotionNotificationRead(notificationId);
if (!cameraDeviceId) return;
const recs = await api.ops.listRecordings().catch(() => ({ recordings: [] }));
@@ -2212,11 +2271,13 @@ const actions = {
},
markAllNotificationsRead() {
markAllNotificationsRead();
void markAllNotificationsRead();
},
clearNotifications() {
setAppState({ motionNotifications: [] });
patchAppState((state) => ({
motionNotifications: state.motionNotifications.filter((notification) => !notification.isRead)
}));
},
refreshClientData() {