Skip to content

Commit 6fa73ff

Browse files
committed
add save button
Signed-off-by: grnd-alt <[email protected]>
1 parent 3d37460 commit 6fa73ff

File tree

8 files changed

+112
-9
lines changed

8 files changed

+112
-9
lines changed

src/App.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type { ResolvablePromise } from '@excalidraw/excalidraw/types/utils'
2929
import type { NonDeletedExcalidrawElement } from '@excalidraw/excalidraw/types/element/types'
3030
import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js'
3131
import { useExcalidrawLang } from './hooks/useExcalidrawLang'
32+
import SaveStatus from './SaveStatus'
3233

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

4849
const [viewModeEnabled, setViewModeEnabled] = useState(isEmbedded)
50+
const [roomDataSaved, setRoomDataSaved] = useState(true)
4951
const [zenModeEnabled] = useState(isEmbedded)
5052
const [gridModeEnabled] = useState(false)
5153

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

95-
if (excalidrawAPI && !collab) { setCollab(new Collab(excalidrawAPI, fileId, publicSharingToken, setViewModeEnabled)) }
96-
if (collab && !collab.portal.socket) collab.startCollab()
98+
if (excalidrawAPI && !collab) { setCollab(new Collab(excalidrawAPI, fileId, publicSharingToken, setViewModeEnabled, setRoomDataSaved)) }
99+
if (collab && !collabStarted) {
100+
setCollabStarted(true)
101+
collab.startCollab()
102+
}
103+
104+
const [isSmartPickerInserted, setIsSmartPickerInserted] = useState(false)
97105
useEffect(() => {
106+
if (isSmartPickerInserted) return
98107
const extraTools = document.getElementsByClassName(
99108
'App-toolbar__extra-tools-trigger',
100109
)[0]
@@ -107,6 +116,7 @@ export default function App({
107116
)
108117
const root = createRoot(smartPick)
109118
root.render(renderSmartPicker())
119+
setIsSmartPickerInserted(true)
110120
}
111121
})
112122

@@ -236,12 +246,26 @@ export default function App({
236246
)
237247
}
238248

249+
const renderTopRightUI = () => {
250+
if (collab?.portal.socket) {
251+
return (
252+
<SaveStatus saving={!(roomDataSaved)} onClick={
253+
() => {
254+
collab?.portal.requestStoreToServer()
255+
}
256+
}
257+
/>
258+
)
259+
}
260+
}
261+
239262
return (
240263
<div className="App">
241264
<div className="excalidraw-wrapper">
242265
<Excalidraw
243266
validateEmbeddable={() => true}
244267
renderEmbeddable={Embeddable}
268+
renderTopRightUI={renderTopRightUI}
245269
excalidrawAPI={(api: ExcalidrawImperativeAPI) => {
246270
console.log(api)
247271
console.log('Setting API')

src/SaveStatus.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
import SaveStatus from './SaveStatus.vue'
6+
import VueWrapper from './VueWrapper'
7+
8+
export default function(props:{saving: Boolean}) {
9+
return React.createElement(VueWrapper, { componentProps: props, component: SaveStatus })
10+
}

src/SaveStatus.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<template>
2+
<NcButton type="tertiary" v-on:click="onClick">
3+
<template #icon>
4+
<NcSavingIndicatorIcon :saving="saving" />
5+
</template>
6+
</NcButton>
7+
</template>
8+
9+
<script>
10+
import { NcButton, NcSavingIndicatorIcon } from '@nextcloud/vue'
11+
export default {
12+
name: "SaveStatus",
13+
components: {
14+
NcButton,
15+
NcSavingIndicatorIcon,
16+
},
17+
props: {
18+
saving: {
19+
type: Boolean,
20+
default: false
21+
},
22+
onClick: {
23+
type: Function
24+
}
25+
},
26+
}
27+
</script>

src/collaboration/Portal.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ export class Portal {
114114
this.socket.on('client-broadcast', (data) =>
115115
this.handleClientBroadcast(data),
116116
)
117+
118+
this.socket.on('room-data-saved', () => this.handleRoomDataSaved())
119+
}
120+
121+
async handleRoomDataSaved() {
122+
this.collab.setRoomDataSaved(true)
117123
}
118124

119125
async handleReadOnlySocket() {
@@ -240,6 +246,10 @@ export class Portal {
240246
await this._broadcastSocketData(data, true)
241247
}
242248

249+
async requestStoreToServer() {
250+
this.socket?.emit('store-to-server', this.roomId)
251+
}
252+
243253
async sendImageFiles(files: BinaryFiles) {
244254
Object.values(files).forEach(file => {
245255
this.collab.addFile(file)

src/collaboration/collab.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@ export class Collab {
1717
portal: Portal
1818
publicSharingToken: string | null
1919
setViewModeEnabled: React.Dispatch<React.SetStateAction<boolean>>
20+
setRoomDataSaved: React.Dispatch<React.SetStateAction<boolean>>
2021
lastBroadcastedOrReceivedSceneVersion: number = -1
2122
private collaborators = new Map<string, Collaborator>()
2223
private files = new Map<string, BinaryFileData>()
2324

24-
constructor(excalidrawAPI: ExcalidrawImperativeAPI, fileId: number, publicSharingToken: string | null, setViewModeEnabled: React.Dispatch<React.SetStateAction<boolean>>) {
25+
constructor(excalidrawAPI: ExcalidrawImperativeAPI, fileId: number, publicSharingToken: string | null, setViewModeEnabled: React.Dispatch<React.SetStateAction<boolean>>, setRoomDataSaved: React.Dispatch<React.SetStateAction<boolean>>) {
2526
this.excalidrawAPI = excalidrawAPI
2627
this.fileId = fileId
2728
this.publicSharingToken = publicSharingToken
2829
this.setViewModeEnabled = setViewModeEnabled
30+
this.setRoomDataSaved = setRoomDataSaved
2931

3032
this.portal = new Portal(`${fileId}`, this, publicSharingToken)
3133
}
@@ -55,6 +57,7 @@ export class Collab {
5557
elements,
5658
},
5759
)
60+
this.setRoomDataSaved(false)
5861
}
5962

6063
private getLastBroadcastedOrReceivedSceneVersion = () => {
@@ -69,6 +72,7 @@ export class Collab {
6972
this.lastBroadcastedOrReceivedSceneVersion = hashElementsVersion(elements)
7073
throttle(() => {
7174
this.portal.broadcastScene('SCENE_INIT', elements)
75+
this.setRoomDataSaved(false)
7276

7377
const syncedFiles = Array.from(this.files.keys())
7478
const newFiles = Object.keys(files).filter((id) => !syncedFiles.includes(id)).reduce((acc, id) => {

websocket_server/AppManager.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
import dotenv from 'dotenv'
77
import express from 'express'
88
import PrometheusDataManager from './PrometheusDataManager.js'
9+
import StorageManager from './StorageManager.js'
910

1011
dotenv.config()
1112

1213
export default class AppManager {
1314

15+
/** @param {StorageManager} storageManager*/
1416
constructor(storageManager) {
1517
this.app = express()
1618
this.storageManager = storageManager

websocket_server/SocketManager.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@
55
* SPDX-License-Identifier: AGPL-3.0-or-later
66
*/
77

8-
import { Server as SocketIO } from 'socket.io'
8+
import { Socket, Server as SocketIO } from 'socket.io'
99
import prometheusMetrics from 'socket.io-prometheus'
1010
import jwt from 'jsonwebtoken'
1111
import dotenv from 'dotenv'
1212
import Utils from './Utils.js'
1313
import { createAdapter } from '@socket.io/redis-streams-adapter'
1414
import SocketDataManager from './SocketDataManager.js'
15+
import StorageManager from './StorageManager.js'
1516

1617
dotenv.config()
1718

1819
export default class SocketManager {
1920

21+
/** @param {StorageManager} storageManager */
2022
constructor(server, roomDataManager, storageManager) {
2123
this.roomDataManager = roomDataManager
2224
this.storageManager = storageManager
@@ -92,7 +94,7 @@ export default class SocketManager {
9294
},
9395
)
9496
next(new Error('Connection verified'))
95-
} catch (e) {}
97+
} catch (e) { }
9698

9799
next(new Error('Authentication error'))
98100
}
@@ -107,6 +109,7 @@ export default class SocketManager {
107109
socket.on('server-volatile-broadcast', (roomID, encryptedData) =>
108110
this.serverVolatileBroadcastHandler(socket, roomID, encryptedData),
109111
)
112+
socket.on('store-to-server', (roomID) => this.storeToServerHandler(roomID, socket))
110113
socket.on('image-add', (roomID, id, data) => this.imageAddHandler(socket, roomID, id, data))
111114
socket.on('image-remove', (roomID, id, data) => this.imageRemoveHandler(socket, roomID, id, data))
112115
socket.on('image-get', (roomID, id, data) => this.imageGetHandler(socket, roomID, id, data))
@@ -117,6 +120,17 @@ export default class SocketManager {
117120
socket.on('disconnect', () => this.handleDisconnect(socket))
118121
}
119122

123+
/**
124+
* @param {int} roomID
125+
* @param {Socket} socket
126+
*/
127+
async storeToServerHandler(roomID, socket) {
128+
this.storageManager.saveRoomDataToServer(roomID).then(() => {
129+
socket.emit("room-data-saved", roomID)
130+
socket.broadcast.to(roomID).emit("room-data-saved", roomID)
131+
})
132+
}
133+
120134
async handleDisconnect(socket) {
121135
await this.socketDataManager.deleteSocketData(socket.id)
122136
socket.removeAllListeners()
@@ -213,7 +227,9 @@ export default class SocketManager {
213227
socketData.user.id,
214228
)
215229
}
216-
230+
/**
231+
* @param {Socket} socket
232+
*/
217233
async serverVolatileBroadcastHandler(socket, roomID, encryptedData) {
218234
const payload = JSON.parse(
219235
Utils.convertArrayBufferToString(encryptedData),

websocket_server/StorageManager.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@
88
import StorageStrategy from './StorageStrategy.js'
99
import LRUCacheStrategy from './LRUCacheStrategy.js'
1010
import RedisStrategy from './RedisStrategy.js'
11+
import ApiService from './ApiService.js';
12+
import Room from './Room.js'
1113

1214
export default class StorageManager {
13-
14-
constructor(strategy) {
15+
/** @param {StorageStrategy} strategy
16+
* @param {ApiService} apiService
17+
*/
18+
constructor(strategy, apiService) {
1519
this.setStrategy(strategy)
20+
this.apiService = apiService
1621
}
1722

1823
setStrategy(strategy) {
@@ -38,6 +43,11 @@ export default class StorageManager {
3843
async clear() {
3944
await this.strategy.clear()
4045
}
46+
/** @param { number }roomId */
47+
async saveRoomDataToServer(roomId) {
48+
const room = await this.get(roomId)
49+
this.apiService.saveRoomDataToServer(roomId, room.data, room.lastEditedUser, room.files);
50+
}
4151

4252
getRooms() {
4353
return this.strategy.getRooms()
@@ -58,7 +68,7 @@ export default class StorageManager {
5868
throw new Error('Invalid storage strategy type')
5969
}
6070

61-
return new StorageManager(strategy)
71+
return new StorageManager(strategy,apiService)
6272
}
6373

6474
}

0 commit comments

Comments
 (0)