feat(web): enable websocket (#3765)
* send store event to page * fix format * add new asset to existing bucket * format * debouncing * format * load bucket * feedback * feat: listen to deletes and auto-subscribe on all asset grid pages * feat: auto refresh on person thumbnail * chore: skip upload event for now * fix: person thumbnail event * fix merge * update handleAssetDeletion with websocket communication * update info box on mount * fix test * fix test * feat: event for trash asset --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { api, AssetApiGetTimeBucketsRequest, AssetResponseDto } from '@api';
|
||||
import { writable } from 'svelte/store';
|
||||
import { throttle } from 'lodash-es';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Unsubscriber, writable } from 'svelte/store';
|
||||
import { handleError } from '../utils/handle-error';
|
||||
import { websocketStore } from './websocket';
|
||||
|
||||
export enum BucketPosition {
|
||||
Above = 'above',
|
||||
@@ -34,11 +37,33 @@ export class AssetBucket {
|
||||
position!: BucketPosition;
|
||||
}
|
||||
|
||||
const isMismatched = (option: boolean | undefined, value: boolean): boolean =>
|
||||
option === undefined ? false : option !== value;
|
||||
|
||||
const THUMBNAIL_HEIGHT = 235;
|
||||
|
||||
interface AddAsset {
|
||||
type: 'add';
|
||||
value: AssetResponseDto;
|
||||
}
|
||||
|
||||
interface DeleteAsset {
|
||||
type: 'delete';
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface TrashAsset {
|
||||
type: 'trash';
|
||||
value: string;
|
||||
}
|
||||
|
||||
type PendingChange = AddAsset | DeleteAsset | TrashAsset;
|
||||
|
||||
export class AssetStore {
|
||||
private store$ = writable(this);
|
||||
private assetToBucket: Record<string, AssetLookup> = {};
|
||||
private pendingChanges: PendingChange[] = [];
|
||||
private unsubscribers: Unsubscriber[] = [];
|
||||
|
||||
initialized = false;
|
||||
timelineHeight = 0;
|
||||
@@ -52,6 +77,63 @@ export class AssetStore {
|
||||
|
||||
subscribe = this.store$.subscribe;
|
||||
|
||||
connect() {
|
||||
this.unsubscribers.push(
|
||||
websocketStore.onUploadSuccess.subscribe((value) => {
|
||||
if (value) {
|
||||
this.pendingChanges.push({ type: 'add', value });
|
||||
this.processPendingChanges();
|
||||
}
|
||||
}),
|
||||
|
||||
websocketStore.onAssetTrash.subscribe((ids) => {
|
||||
console.log('onAssetTrash', ids);
|
||||
if (ids) {
|
||||
for (const id of ids) {
|
||||
this.pendingChanges.push({ type: 'trash', value: id });
|
||||
}
|
||||
this.processPendingChanges();
|
||||
}
|
||||
}),
|
||||
|
||||
websocketStore.onAssetDelete.subscribe((value) => {
|
||||
if (value) {
|
||||
this.pendingChanges.push({ type: 'delete', value });
|
||||
this.processPendingChanges();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
for (const unsubscribe of this.unsubscribers) {
|
||||
unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
processPendingChanges = throttle(() => {
|
||||
for (const { type, value } of this.pendingChanges) {
|
||||
switch (type) {
|
||||
case 'add':
|
||||
this.addAsset(value);
|
||||
break;
|
||||
|
||||
case 'trash':
|
||||
if (!this.options.isTrashed) {
|
||||
this.removeAsset(value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
this.removeAsset(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.pendingChanges = [];
|
||||
this.emit(true);
|
||||
}, 10_000);
|
||||
|
||||
async init(viewport: Viewport) {
|
||||
this.initialized = false;
|
||||
this.timelineHeight = 0;
|
||||
@@ -168,6 +250,46 @@ export class AssetStore {
|
||||
return scrollTimeline ? delta : 0;
|
||||
}
|
||||
|
||||
private addAsset(asset: AssetResponseDto): void {
|
||||
if (
|
||||
this.assetToBucket[asset.id] ||
|
||||
this.options.userId ||
|
||||
this.options.personId ||
|
||||
this.options.albumId ||
|
||||
isMismatched(this.options.isArchived, asset.isArchived) ||
|
||||
isMismatched(this.options.isFavorite, asset.isFavorite)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeBucket = DateTime.fromISO(asset.fileCreatedAt).toUTC().startOf('month').toString();
|
||||
let bucket = this.getBucketByDate(timeBucket);
|
||||
|
||||
if (!bucket) {
|
||||
bucket = {
|
||||
bucketDate: timeBucket,
|
||||
bucketHeight: THUMBNAIL_HEIGHT,
|
||||
assets: [],
|
||||
cancelToken: null,
|
||||
position: BucketPosition.Unknown,
|
||||
};
|
||||
|
||||
this.buckets.push(bucket);
|
||||
this.buckets = this.buckets.sort((a, b) => {
|
||||
const aDate = DateTime.fromISO(a.bucketDate).toUTC();
|
||||
const bDate = DateTime.fromISO(b.bucketDate).toUTC();
|
||||
return bDate.diff(aDate).milliseconds;
|
||||
});
|
||||
}
|
||||
|
||||
bucket.assets.push(asset);
|
||||
bucket.assets.sort((a, b) => {
|
||||
const aDate = DateTime.fromISO(a.fileCreatedAt).toUTC();
|
||||
const bDate = DateTime.fromISO(b.fileCreatedAt).toUTC();
|
||||
return bDate.diff(aDate).milliseconds;
|
||||
});
|
||||
}
|
||||
|
||||
getBucketByDate(bucketDate: string): AssetBucket | null {
|
||||
return this.buckets.find((bucket) => bucket.bucketDate === bucketDate) || null;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import type { AssetResponseDto, ServerVersionResponseDto } from '@api';
|
||||
import { io } from 'socket.io-client';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let websocket: Socket;
|
||||
export const websocketStore = {
|
||||
onUploadSuccess: writable<AssetResponseDto>(),
|
||||
onAssetDelete: writable<string>(),
|
||||
onAssetTrash: writable<string[]>(),
|
||||
onPersonThumbnail: writable<string>(),
|
||||
serverVersion: writable<ServerVersionResponseDto>(),
|
||||
connected: writable<boolean>(false),
|
||||
};
|
||||
|
||||
export const openWebsocketConnection = () => {
|
||||
try {
|
||||
websocket = io('', {
|
||||
const websocket = io('', {
|
||||
path: '/api/socket.io',
|
||||
transports: ['polling'],
|
||||
reconnection: true,
|
||||
@@ -12,21 +21,18 @@ export const openWebsocketConnection = () => {
|
||||
autoConnect: true,
|
||||
});
|
||||
|
||||
listenToEvent(websocket);
|
||||
websocket
|
||||
.on('connect', () => websocketStore.connected.set(true))
|
||||
.on('disconnect', () => websocketStore.connected.set(false))
|
||||
// .on('on_upload_success', (data) => websocketStore.onUploadSuccess.set(JSON.parse(data) as AssetResponseDto))
|
||||
.on('on_asset_delete', (data) => websocketStore.onAssetDelete.set(JSON.parse(data) as string))
|
||||
.on('on_asset_trash', (data) => websocketStore.onAssetTrash.set(JSON.parse(data) as string[]))
|
||||
.on('on_person_thumbnail', (data) => websocketStore.onPersonThumbnail.set(JSON.parse(data) as string))
|
||||
.on('on_server_version', (data) => websocketStore.serverVersion.set(JSON.parse(data) as ServerVersionResponseDto))
|
||||
.on('error', (e) => console.log('Websocket Error', e));
|
||||
|
||||
return () => websocket?.close();
|
||||
} catch (e) {
|
||||
console.log('Cannot connect to websocket ', e);
|
||||
}
|
||||
};
|
||||
|
||||
const listenToEvent = (socket: Socket) => {
|
||||
//TODO: if we are not using this, we should probably remove it?
|
||||
socket.on('on_upload_success', () => undefined);
|
||||
|
||||
socket.on('error', (e) => {
|
||||
console.log('Websocket Error', e);
|
||||
});
|
||||
};
|
||||
|
||||
export const closeWebsocketConnection = () => {
|
||||
websocket?.close();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user