fix(webapp): persist motion alerts in activity feed
This commit is contained in:
@@ -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({}) })
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user