Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add save button #263

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "module",
"main": "src/main.tsx",
"scripts": {
"build": "vite --mode production build",
"build": "NODE_OPTIONS='--max-old-space-size=4096' vite --mode production build",
"dev": "vite --mode development build",
"watch": "vite --mode development build --watch",
"lint": "eslint --ext .js,.mjs,.ts,.tsx,.vue src websocket_server tests/integration ",
Expand Down Expand Up @@ -89,4 +89,4 @@
"node": "^20",
"npm": "^10"
}
}
}
28 changes: 26 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type { ResolvablePromise } from '@excalidraw/excalidraw/types/utils'
import type { NonDeletedExcalidrawElement } from '@excalidraw/excalidraw/types/element/types'
import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js'
import { useExcalidrawLang } from './hooks/useExcalidrawLang'
import SaveStatus from './SaveStatus'

interface WhiteboardAppProps {
fileId: number
Expand All @@ -46,6 +47,7 @@ export default function App({
const fileNameWithoutExtension = fileName.split('.').slice(0, -1).join('.')

const [viewModeEnabled, setViewModeEnabled] = useState(isEmbedded)
const [roomDataSaved, setRoomDataSaved] = useState(true)
const [zenModeEnabled] = useState(isEmbedded)
const [gridModeEnabled] = useState(false)

Expand Down Expand Up @@ -91,10 +93,17 @@ export default function App({
const [excalidrawAPI, setExcalidrawAPI]
= useState<ExcalidrawImperativeAPI | null>(null)
const [collab, setCollab] = useState<Collab | null>(null)
const [collabStarted, setCollabStarted] = useState(false)

if (excalidrawAPI && !collab) { setCollab(new Collab(excalidrawAPI, fileId, publicSharingToken, setViewModeEnabled)) }
if (collab && !collab.portal.socket) collab.startCollab()
if (excalidrawAPI && !collab) { setCollab(new Collab(excalidrawAPI, fileId, publicSharingToken, setViewModeEnabled, setRoomDataSaved)) }
if (collab && !collabStarted) {
setCollabStarted(true)
collab.startCollab()
}

const [isSmartPickerInserted, setIsSmartPickerInserted] = useState(false)
useEffect(() => {
if (isSmartPickerInserted) return
const extraTools = document.getElementsByClassName(
'App-toolbar__extra-tools-trigger',
)[0]
Expand All @@ -107,6 +116,7 @@ export default function App({
)
const root = createRoot(smartPick)
root.render(renderSmartPicker())
setIsSmartPickerInserted(true)
}
})

Expand Down Expand Up @@ -236,12 +246,26 @@ export default function App({
)
}

const renderTopRightUI = () => {
if (collab?.portal.socket) {
return (
<SaveStatus saving={!(roomDataSaved)} onClick={
() => {
collab?.portal.requestStoreToServer()
}
}
/>
)
}
}

return (
<div className="App">
<div className="excalidraw-wrapper">
<Excalidraw
validateEmbeddable={() => true}
renderEmbeddable={Embeddable}
renderTopRightUI={renderTopRightUI}
excalidrawAPI={(api: ExcalidrawImperativeAPI) => {
console.log(api)
console.log('Setting API')
Expand Down
10 changes: 10 additions & 0 deletions src/SaveStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import SaveStatus from './SaveStatus.vue'
import VueWrapper from './VueWrapper'

export default function(props:{saving: boolean}) {
return React.createElement(VueWrapper, { componentProps: props, component: SaveStatus })
}
32 changes: 32 additions & 0 deletions src/SaveStatus.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
<template>
<NcButton type="tertiary" @click="onClick">
<template #icon>
<NcSavingIndicatorIcon :saving="saving" />
</template>
</NcButton>
</template>

<script>
import { NcButton, NcSavingIndicatorIcon } from '@nextcloud/vue'
export default {
name: 'SaveStatus',
components: {
NcButton,
NcSavingIndicatorIcon,
},
props: {
saving: {
type: Boolean,
default: false,
},
onClick: {
type: Function,
default: () => { },
},
},
}
</script>
10 changes: 10 additions & 0 deletions src/collaboration/Portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ export class Portal {
this.socket.on('client-broadcast', (data) =>
this.handleClientBroadcast(data),
)

this.socket.on('room-data-saved', () => this.handleRoomDataSaved())
}

async handleRoomDataSaved() {
this.collab.setRoomDataSaved(true)
}

async handleReadOnlySocket() {
Expand Down Expand Up @@ -240,6 +246,10 @@ export class Portal {
await this._broadcastSocketData(data, true)
}

async requestStoreToServer() {
this.socket?.emit('store-to-server', this.roomId)
}

async sendImageFiles(files: BinaryFiles) {
Object.values(files).forEach(file => {
this.collab.addFile(file)
Expand Down
6 changes: 5 additions & 1 deletion src/collaboration/collab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ export class Collab {
portal: Portal
publicSharingToken: string | null
setViewModeEnabled: React.Dispatch<React.SetStateAction<boolean>>
setRoomDataSaved: React.Dispatch<React.SetStateAction<boolean>>
lastBroadcastedOrReceivedSceneVersion: number = -1
private collaborators = new Map<string, Collaborator>()
private files = new Map<string, BinaryFileData>()

constructor(excalidrawAPI: ExcalidrawImperativeAPI, fileId: number, publicSharingToken: string | null, setViewModeEnabled: React.Dispatch<React.SetStateAction<boolean>>) {
constructor(excalidrawAPI: ExcalidrawImperativeAPI, fileId: number, publicSharingToken: string | null, setViewModeEnabled: React.Dispatch<React.SetStateAction<boolean>>, setRoomDataSaved: React.Dispatch<React.SetStateAction<boolean>>) {
this.excalidrawAPI = excalidrawAPI
this.fileId = fileId
this.publicSharingToken = publicSharingToken
this.setViewModeEnabled = setViewModeEnabled
this.setRoomDataSaved = setRoomDataSaved

this.portal = new Portal(`${fileId}`, this, publicSharingToken)
}
Expand Down Expand Up @@ -55,6 +57,7 @@ export class Collab {
elements,
},
)
this.setRoomDataSaved(false)
}

private getLastBroadcastedOrReceivedSceneVersion = () => {
Expand All @@ -69,6 +72,7 @@ export class Collab {
this.lastBroadcastedOrReceivedSceneVersion = hashElementsVersion(elements)
throttle(() => {
this.portal.broadcastScene('SCENE_INIT', elements)
this.setRoomDataSaved(false)

const syncedFiles = Array.from(this.files.keys())
const newFiles = Object.keys(files).filter((id) => !syncedFiles.includes(id)).reduce((acc, id) => {
Expand Down
16 changes: 14 additions & 2 deletions websocket_server/SocketManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { Server as SocketIO } from 'socket.io'
import { Socket, Server as SocketIO } from 'socket.io'
import prometheusMetrics from 'socket.io-prometheus'
import jwt from 'jsonwebtoken'
import dotenv from 'dotenv'
Expand Down Expand Up @@ -92,7 +92,7 @@ export default class SocketManager {
},
)
next(new Error('Connection verified'))
} catch (e) {}
} catch (e) { }

next(new Error('Authentication error'))
}
Expand All @@ -107,6 +107,7 @@ export default class SocketManager {
socket.on('server-volatile-broadcast', (roomID, encryptedData) =>
this.serverVolatileBroadcastHandler(socket, roomID, encryptedData),
)
socket.on('store-to-server', (roomID) => this.storeToServerHandler(roomID, socket))
socket.on('image-add', (roomID, id, data) => this.imageAddHandler(socket, roomID, id, data))
socket.on('image-remove', (roomID, id, data) => this.imageRemoveHandler(socket, roomID, id, data))
socket.on('image-get', (roomID, id, data) => this.imageGetHandler(socket, roomID, id, data))
Expand All @@ -117,6 +118,17 @@ export default class SocketManager {
socket.on('disconnect', () => this.handleDisconnect(socket))
}

/**
* @param {number} roomID roomID
* @param {Socket} socket socket
*/
async storeToServerHandler(roomID, socket) {
this.storageManager.saveRoomDataToServer(roomID).then(() => {
socket.emit('room-data-saved', roomID)
socket.broadcast.to(roomID).emit('room-data-saved', roomID)
})
}

async handleDisconnect(socket) {
await this.socketDataManager.deleteSocketData(socket.id)
socket.removeAllListeners()
Expand Down
18 changes: 16 additions & 2 deletions websocket_server/StorageManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@
import StorageStrategy from './StorageStrategy.js'
import LRUCacheStrategy from './LRUCacheStrategy.js'
import RedisStrategy from './RedisStrategy.js'
import ApiService from './ApiService.js'

export default class StorageManager {

constructor(strategy) {
/**
* @param {StorageStrategy} strategy StorageStrategy
* @param {ApiService} apiService ApiService
*/
constructor(strategy, apiService) {
this.setStrategy(strategy)
this.apiService = apiService
}

setStrategy(strategy) {
Expand All @@ -39,6 +45,14 @@ export default class StorageManager {
await this.strategy.clear()
}

/**
* @param { number } roomId roomId
*/
async saveRoomDataToServer(roomId) {
const room = await this.get(roomId)
this.apiService.saveRoomDataToServer(roomId, room.data, room.lastEditedUser, room.files)
}

getRooms() {
return this.strategy.getRooms()
}
Expand All @@ -58,7 +72,7 @@ export default class StorageManager {
throw new Error('Invalid storage strategy type')
}

return new StorageManager(strategy)
return new StorageManager(strategy, apiService)
}

}
Loading