Extract web app client controller helpers
This commit is contained in:
180
WebApp/src/lib/app/controller-client.js
Normal file
180
WebApp/src/lib/app/controller-client.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
|
||||
export const createControllerClientModule = ({
|
||||
api,
|
||||
getAppState,
|
||||
setAppState,
|
||||
patchAppState,
|
||||
makeId
|
||||
}) => {
|
||||
const getLinkedCamera = (cameraDeviceId) =>
|
||||
getAppState().linkedCameras.find((camera) => camera.cameraDeviceId === cameraDeviceId);
|
||||
|
||||
const getCameraLabel = (cameraDeviceId, cameraName) => {
|
||||
const explicitName = typeof cameraName === 'string' ? cameraName.trim() : '';
|
||||
if (explicitName) return explicitName;
|
||||
|
||||
const linkedName = getLinkedCamera(cameraDeviceId)?.cameraName;
|
||||
if (typeof linkedName === 'string' && linkedName.trim()) {
|
||||
return linkedName.trim();
|
||||
}
|
||||
|
||||
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(),
|
||||
isRead: false
|
||||
};
|
||||
|
||||
patchAppState((state) => ({
|
||||
motionNotifications: [notification, ...state.motionNotifications].slice(0, 50)
|
||||
}));
|
||||
|
||||
void refreshMotionNotifications();
|
||||
};
|
||||
|
||||
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 = 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) => {
|
||||
setAppState({
|
||||
recordingModal: {
|
||||
open: true,
|
||||
title: title || 'Recording Playback',
|
||||
url: downloadUrl
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeRecordingModal = () => {
|
||||
setAppState({
|
||||
recordingModal: {
|
||||
open: false,
|
||||
title: 'Recording Playback',
|
||||
url: ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const pollClientData = async () => {
|
||||
const { device } = getAppState();
|
||||
if (!device || device.role !== 'client') return;
|
||||
|
||||
const [recs, links, deviceList, notifications] = await Promise.all([
|
||||
api.ops.listRecordings().catch(() => ({ recordings: [] })),
|
||||
api.devices.listLinks().catch(() => ({ links: [] })),
|
||||
api.devices.list().catch(() => ({ devices: [] })),
|
||||
api.ops.listNotifications().catch(() => ({ notifications: [] }))
|
||||
]);
|
||||
|
||||
const cameraById = new Map(
|
||||
(deviceList.devices || [])
|
||||
.filter((entry) => entry.role === 'camera')
|
||||
.map((entry) => [entry.id, entry])
|
||||
);
|
||||
|
||||
const linkedCameras = (links.links || []).map((link) => {
|
||||
const camera = cameraById.get(link.cameraDeviceId);
|
||||
return {
|
||||
...link,
|
||||
cameraName: camera?.name ?? null,
|
||||
cameraStatus: camera?.status ?? 'offline'
|
||||
};
|
||||
});
|
||||
|
||||
setAppState({
|
||||
recordings: recs.recordings || [],
|
||||
linkedCameras
|
||||
});
|
||||
syncMotionNotificationsFromDeliveries(notifications.notifications);
|
||||
};
|
||||
|
||||
return {
|
||||
getLinkedCamera,
|
||||
getCameraLabel,
|
||||
markMotionNotificationRead,
|
||||
markAllNotificationsRead,
|
||||
pushMotionNotification,
|
||||
openRecordingModal,
|
||||
closeRecordingModal,
|
||||
pollClientData
|
||||
};
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getMotionDetectionProfile,
|
||||
pageFromPath
|
||||
} from './controller-shared';
|
||||
import { createControllerClientModule } from './controller-client';
|
||||
import { createControllerMediaModule } from './controller-media';
|
||||
import { getAppState, patchAppState, resetAppState, setAppState } from './store';
|
||||
|
||||
@@ -86,6 +87,14 @@ const {
|
||||
patchAppState
|
||||
});
|
||||
|
||||
const clientController = createControllerClientModule({
|
||||
api,
|
||||
getAppState,
|
||||
setAppState,
|
||||
patchAppState,
|
||||
makeId
|
||||
});
|
||||
|
||||
const mediaController = createControllerMediaModule({
|
||||
api,
|
||||
createMotionDetector,
|
||||
@@ -98,6 +107,17 @@ const mediaController = createControllerMediaModule({
|
||||
updateMotionDetectionState
|
||||
});
|
||||
|
||||
const {
|
||||
getLinkedCamera,
|
||||
getCameraLabel,
|
||||
markMotionNotificationRead,
|
||||
markAllNotificationsRead,
|
||||
pushMotionNotification,
|
||||
openRecordingModal,
|
||||
closeRecordingModal,
|
||||
pollClientData
|
||||
} = clientController;
|
||||
|
||||
const {
|
||||
refreshCameraInputDevices,
|
||||
startCameraPreview,
|
||||
@@ -275,130 +295,6 @@ const removeCameraSessionMapping = (streamSessionId) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const getLinkedCamera = (cameraDeviceId) =>
|
||||
getAppState().linkedCameras.find((camera) => camera.cameraDeviceId === cameraDeviceId);
|
||||
|
||||
const getCameraLabel = (cameraDeviceId, cameraName) => {
|
||||
const explicitName = typeof cameraName === 'string' ? cameraName.trim() : '';
|
||||
if (explicitName) return explicitName;
|
||||
|
||||
const linkedName = getLinkedCamera(cameraDeviceId)?.cameraName;
|
||||
if (typeof linkedName === 'string' && linkedName.trim()) {
|
||||
return linkedName.trim();
|
||||
}
|
||||
|
||||
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(),
|
||||
isRead: false
|
||||
};
|
||||
|
||||
patchAppState((state) => ({
|
||||
motionNotifications: [notification, ...state.motionNotifications].slice(0, 50)
|
||||
}));
|
||||
|
||||
void refreshMotionNotifications();
|
||||
};
|
||||
|
||||
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 = 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) => {
|
||||
setAppState({
|
||||
recordingModal: {
|
||||
open: true,
|
||||
title: title || 'Recording Playback',
|
||||
url: downloadUrl
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeRecordingModal = () => {
|
||||
setAppState({
|
||||
recordingModal: {
|
||||
open: false,
|
||||
title: 'Recording Playback',
|
||||
url: ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const teardownPeerConnection = (streamSessionId) => {
|
||||
if (!streamSessionId) {
|
||||
for (const connection of peerConnections.values()) {
|
||||
|
||||
Reference in New Issue
Block a user