import _ from "lodash"; import { action, IObservableArray, IObservableValue, observable, ObservableMap, remove, set } from "mobx"; import { ID, Mouse, MouseClick, Slide, SlideObjectType } from "../shared/types"; import { socket } from "../shared/utils/socket"; export enum SocketActions { ENTER_ROOM = "ENTER_ROOM", LEAVE_ROOM = "LEAVE_ROOM", MOUSE_MOVEMENT = "MOUSE_MOVEMENT", CREATE_SLIDE = "CREATE_SLIDE", DELETE_SLIDES = "DELETE_SLIDES", MOUSE_CLICK = "MOUSE_CLICK", CREATE_SLIDE_OBJECT = "CREATE_SLIDE_OBJECT", UPDATE_SLIDE_OBJECT = "UPDATE_SLIDE_OBJECT", DELETE_SLIDE_OBJECT = "DELETE_SLIDE_OBJECT", } export enum RemoteSocketActions { ENTER_ROOM = "REMOTE_ENTER_ROOM", LEAVE_ROOM = "REMOTE_LEAVE_ROOM", MOUSE_MOVEMENT = "REMOTE_MOUSE_MOVEMENT", CREATE_SLIDE = "REMOTE_CREATE_SLIDE", DELETE_SLIDES = "REMOTE_DELETE_SLIDES", MOUSE_CLICK = "REMOTE_MOUSE_CLICK", CREATE_SLIDE_OBJECT = "REMOTE_CREATE_SLIDE_OBJECT", UPDATE_SLIDE_OBJECT = "REMOTE_UPDATE_SLIDE_OBJECT", DELETE_SLIDE_OBJECT = "REMOTE_DELETE_SLIDE_OBJECT", } const UPDATE = "UPDATE"; const REMOTE_UPDATE = "REMOTE_UPDATE"; export type RemotePayload = { action: RemoteSocketActions; data: any; }; export type MouseMovementData = { x: number; y: number; userId: ID; }; export type EnterRoomData = { userId: ID; }; export type LeaveRoomData = { userId: ID; }; export type CreateSlideData = { id: ID; index: number; content: string; }; export type DeleteSlidesData = { slideIds: ID[]; }; export type MouseClickData = { userId: ID; x: number; y: number; timestamp: number; }; // With these crud operations, we will absolutely run into conflicts. // We're not going to handle them for now export type CreateSlideObjectData = | CreateSlideTextData | CreateSlideAirtableData; export type CreateSlideAirtableData = { type: SlideObjectType.AIRTABLE; } & CreateSlideObjectDataBase; export type CreateSlideTextData = { text: string; type: SlideObjectType.TEXT; } & CreateSlideObjectDataBase; export type CreateSlideObjectDataBase = { id: ID; slideId: ID; type: SlideObjectType; x: number; y: number; }; export type UpdateSlideObjectData = { id: ID; slideId: ID; data: any; }; export type DeleteSlideObjectData = { id: ID; slideId: ID; }; export type SlideshowState = { subscribers: { [userId: string]: ID }; mouseInfo: { [userId: string]: Mouse }; slides: { [slideId: string]: Slide }; }; export type BackgroundStoreProps = { store: SlideshowStore; }; export class SlideshowStore { userId: IObservableValue; roomId: IObservableValue; subscribers: ObservableMap; socket: SocketIOClient.Socket; mouseInfo: ObservableMap; slides: ObservableMap; mouseClicks: ObservableMap; currentSlide: IObservableValue; selectedSlideElement: IObservableValue; isPresenting: IObservableValue; selectedSlides: IObservableArray; static async constructNew(slideshowId: string) { const a = ""; } constructor( userId: ID, slideshowId: ID, initialSlideshowState: SlideshowState ) { const firstSlide = initialSlideshowState.slides && !_.isEmpty(initialSlideshowState.slides) ? Object.keys(initialSlideshowState.slides).reduce((a, b) => Math.min( initialSlideshowState.slides[a].index, initialSlideshowState.slides[b].index ) === initialSlideshowState.slides[a].index ? a : b ) : null; this.userId = observable.box(userId); this.roomId = observable.box(slideshowId); this.socket = socket; // Shared state this.subscribers = observable.map( initialSlideshowState.subscribers ); this.mouseInfo = observable.map(initialSlideshowState.mouseInfo); this.mouseClicks = observable.map({}); this.slides = observable.map(initialSlideshowState.slides); // Local state // TODO: Show current slide across different people this.currentSlide = observable.box(firstSlide); this.selectedSlideElement = observable.box(null); this.isPresenting = observable.box(false); this.selectedSlides = observable.array([]); this.socket.on( `${REMOTE_UPDATE}:${this.roomId}`, (payload: RemotePayload) => { this.handleRemoteAction(payload.action, payload.data); } ); } emit(action: SocketActions, data: any) { this.handleAction(action, data); // console.log("EMITTING"); // console.log(action); // console.log(data); this.socket.emit(UPDATE, { roomId: this.roomId, action: action, data: data, }); } handleAction(action: SocketActions, data: any) { switch (action) { case SocketActions.ENTER_ROOM: this.handleEnterRoom(data); break; case SocketActions.LEAVE_ROOM: this.handleLeaveRoom(data); break; case SocketActions.MOUSE_MOVEMENT: this.handleMouseMovement(data); break; case SocketActions.CREATE_SLIDE: this.handleCreateSlide(data); break; case SocketActions.DELETE_SLIDES: this.handleDeleteSlides(data); break; case SocketActions.MOUSE_CLICK: this.handleMouseClick(data); break; case SocketActions.CREATE_SLIDE_OBJECT: this.handleCreateSlideObject(data); break; case SocketActions.UPDATE_SLIDE_OBJECT: this.handleUpdateSlideObject(data); break; case SocketActions.DELETE_SLIDE_OBJECT: this.handleDeleteSlideObject(data); break; default: this.handleOtherAction(action, data); } } @action handleEnterRoom(data: EnterRoomData) { set(this.subscribers, data.userId, data.userId); } @action handleLeaveRoom(data: LeaveRoomData) { remove(this.slides, data.userId); } @action handleMouseMovement(data: MouseMovementData) { set(this.mouseInfo, data.userId, { x: data.x, y: data.y, userId: data.userId, }); } @action handleCreateSlide(data: CreateSlideData) { set(this.slides, data.id, { id: data.id, index: data.index, content: {}, }); } @action handleDeleteSlides(data: DeleteSlidesData) { data.slideIds.forEach((id) => { this.slides.delete(id); }); } handleCreateSlideObject(data: CreateSlideObjectData) { console.log("?????"); switch (data.type) { case SlideObjectType.TEXT: this.handleCreateText(data as CreateSlideTextData); break; case SlideObjectType.AIRTABLE: this.handleCreateAirtable(data); break; default: console.log("UNHANDLED OBJECT TYPE"); break; } } @action handleCreateAirtable(data: CreateSlideAirtableData) { const currentSlide = this.slides.get(data.slideId)!; this.slides.set(data.slideId, { ...currentSlide, content: { ...currentSlide.content, [data.id]: { ...currentSlide.content[data.id], id: data.id, type: data.type, x: data.x, y: data.y, }, }, }); } @action handleCreateText(data: CreateSlideTextData) { const currentSlide = this.slides.get(data.slideId)!; this.slides.set(data.slideId, { ...currentSlide, content: { ...currentSlide.content, [data.id]: { ...currentSlide.content[data.id], id: data.id, type: data.type, text: data.text, x: data.x, y: data.y, }, }, }); } @action handleUpdateSlideObject(data: UpdateSlideObjectData) { const currentSlide = this.slides.get(data.slideId)!; this.slides.set(data.slideId, { ...currentSlide, content: { ...currentSlide.content, [data.id]: { ...currentSlide.content[data.id], ...data.data, }, }, }); } @action handleDeleteSlideObject(data: DeleteSlideObjectData) { const currentSlide = this.slides.get(data.slideId)!; const { [data.id]: omit, ...contentWithoutSlideObject } = currentSlide.content; this.slides.set(data.slideId, { ...currentSlide, content: contentWithoutSlideObject, }); this.selectedSlideElement.set(null); } handleOtherAction(action: SocketActions | RemoteSocketActions, data: any) { console.log("UNHANDLED ACTION: " + action); } @action setIsPresenting(isPresenting: boolean) { this.isPresenting.set(isPresenting); } @action setSelectedSlides(slideIds: ID[]) { this.selectedSlides.replace(slideIds); } @action nextSlide() { const sortedSlides = Array.from(this.slides.keys()).sort( (a, b) => this.slides.get(a)!.index - this.slides.get(b)!.index ); const currentIndex = sortedSlides.indexOf(this.currentSlide.get()!); if (currentIndex === sortedSlides.length - 1) { return; } this.currentSlide.set(sortedSlides[currentIndex + 1]); } @action previousSlide() { const sortedSlides = Array.from(this.slides.keys()).sort( (a, b) => this.slides.get(a)!.index - this.slides.get(b)!.index ); const currentIndex = sortedSlides.indexOf(this.currentSlide.get()!); if (currentIndex === 0) { return; } this.currentSlide.set(sortedSlides[currentIndex - 1]); } handleRemoteAction(action: RemoteSocketActions, data: any) { // Don't duplicate actions done locally, but there // might be a better way to do this if (data.userId === this.userId) { return; } switch (action) { case RemoteSocketActions.ENTER_ROOM: this.handleEnterRoom(data); break; case RemoteSocketActions.LEAVE_ROOM: this.handleLeaveRoom(data); break; case RemoteSocketActions.MOUSE_MOVEMENT: this.handleMouseMovement(data); break; case RemoteSocketActions.CREATE_SLIDE: this.handleCreateSlide(data); break; case RemoteSocketActions.DELETE_SLIDES: this.handleDeleteSlides(data); break; case RemoteSocketActions.MOUSE_CLICK: this.handleMouseClick(data); break; case RemoteSocketActions.CREATE_SLIDE_OBJECT: this.handleCreateSlideObject(data); break; case RemoteSocketActions.UPDATE_SLIDE_OBJECT: this.handleUpdateSlideObject(data); break; case RemoteSocketActions.DELETE_SLIDE_OBJECT: this.handleDeleteSlideObject(data); break; default: this.handleOtherAction(action, data); } } @action handleMouseClick(data: MouseClickData) { set(this.mouseClicks, data.userId, { x: data.x, y: data.y, timestamp: data.timestamp, userId: data.userId, }); } }