From a4ddcf1ad888a65f5eee9aab46d6c7c6fdcd04c5 Mon Sep 17 00:00:00 2001 From: Shivangi Chaurasia Date: Thu, 28 May 2026 22:29:34 +0530 Subject: [PATCH 1/5] feat: implement File Lock & Edit Access Request System --- package-lock.json | 42 ++----- server.js | 181 +++++++++++++++++++++++++++ src/Actions.js | 8 ++ src/App.css | 135 ++++++++++++++++++++ src/components/Editor.js | 91 +++++++++++++- src/components/FileExplorer.js | 29 ++++- src/components/FileTabs.js | 13 +- src/components/RequestAccessModal.js | 15 ++- src/pages/EditorPage.js | 139 +++++++++++++++++++- 9 files changed, 605 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 35bbde5..4ef4918 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,7 +92,6 @@ "node_modules/@babel/core": { "version": "7.27.4", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -638,7 +637,6 @@ "node_modules/@babel/plugin-syntax-flow": { "version": "7.27.1", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1390,7 +1388,6 @@ "node_modules/@babel/plugin-transform-react-jsx": { "version": "7.27.1", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", @@ -1773,7 +1770,6 @@ "node_modules/@babel/runtime": { "version": "7.27.6", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -3116,6 +3112,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3136,6 +3133,7 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -3145,7 +3143,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", @@ -3214,7 +3213,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -3497,7 +3497,6 @@ "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.62.0", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -3547,7 +3546,6 @@ "node_modules/@typescript-eslint/parser": { "version": "5.62.0", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -3900,7 +3898,6 @@ "node_modules/acorn": { "version": "8.15.0", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3971,7 +3968,6 @@ "node_modules/ajv": { "version": "6.12.6", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4769,7 +4765,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -5119,8 +5114,7 @@ "version": "5.65.19", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.19.tgz", "integrity": "sha512-+aFkvqhaAVr1gferNMuN8vkTSrWIFvzlMV9I2KBLCWS2WpZ2+UAkZjlMZmEuT+gcXTi6RrGQCkWq1/bDtGqhIA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/collect-v8-coverage": { "version": "1.0.2", @@ -5369,7 +5363,6 @@ "version": "3.43.0", "hasInstallScript": true, "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -5766,8 +5759,7 @@ }, "node_modules/csstype": { "version": "3.1.3", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -5963,6 +5955,7 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -6621,7 +6614,6 @@ "node_modules/eslint": { "version": "8.57.1", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9146,7 +9138,6 @@ "node_modules/jest": { "version": "27.5.1", "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -10510,6 +10501,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -11467,7 +11459,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12513,7 +12504,6 @@ "node_modules/postcss-selector-parser": { "version": "6.1.2", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -12673,7 +12663,6 @@ "node_modules/prop-types": { "version": "15.8.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -12829,7 +12818,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12966,7 +12954,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13010,7 +12997,6 @@ "node_modules/react-refresh": { "version": "0.11.0", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13432,7 +13418,6 @@ "node_modules/rollup": { "version": "2.79.2", "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -13667,7 +13652,6 @@ "node_modules/schema-utils/node_modules/ajv": { "version": "8.17.1", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15283,7 +15267,6 @@ "node_modules/type-fest": { "version": "0.21.3", "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -15659,7 +15642,6 @@ "node_modules/webpack": { "version": "5.99.9", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -15726,7 +15708,6 @@ "node_modules/webpack-dev-server": { "version": "4.15.2", "license": "MIT", - "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -16119,7 +16100,6 @@ "node_modules/workbox-build/node_modules/ajv": { "version": "8.17.1", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16537,7 +16517,6 @@ "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.30.tgz", "integrity": "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==", "license": "MIT", - "peer": true, "dependencies": { "lib0": "^0.2.99" }, @@ -16566,7 +16545,6 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/server.js b/server.js index 6e4460c..3118f5d 100644 --- a/server.js +++ b/server.js @@ -123,6 +123,8 @@ io.on('connection', (socket) => { fileSystem: createDefaultFileSystem(), fileContents: {}, // stores uploaded file contents keyed by fileId chatMessages: [], // stores group chat messages + fileLocks: {}, // Key: fileId, Value: { socketId, userName, allowedUsers } + activeEditors: {}, // Key: fileId, Value: { socketId, userName } }; // Send the ownership token ONLY to the admin socket. // No other client ever receives this value. @@ -159,6 +161,12 @@ io.on('connection', (socket) => { fileContents: roomState[roomId].fileContents, }); + // Sync file locks and active editors + socket.emit(ACTIONS.LOCK_STATUS_UPDATE, { + fileLocks: roomState[roomId].fileLocks || {}, + activeEditors: roomState[roomId].activeEditors || {} + }); + // Sync chat history to newly joined client if (roomState[roomId].chatMessages.length > 0) { socket.emit(ACTIONS.CHAT_RECEIVE, roomState[roomId].chatMessages); @@ -355,6 +363,140 @@ io.on('connection', (socket) => { io.to(roomId).emit(ACTIONS.CHAT_RECEIVE, chatMsg); }); + // ---- File Lock & Edit Access Request System ---- + socket.on(ACTIONS.FILE_LOCKED, ({ roomId, fileId }) => { + if (!roomState[roomId]) return; + const room = roomState[roomId]; + const isRoomAdmin = room.admin === socket.id; + const hasWritePermission = room.permissions[socket.id] === true; + + if (!hasWritePermission && !isRoomAdmin) { + socket.emit(ACTIONS.PERMISSION_DENIED, { message: 'You do not have permission to lock files in this room.' }); + return; + } + + // Check if already locked by someone else (only admin can override) + const existingLock = room.fileLocks?.[fileId]; + if (existingLock && existingLock.socketId !== socket.id && !isRoomAdmin) { + socket.emit(ACTIONS.PERMISSION_DENIED, { message: 'This file is already locked by someone else.' }); + return; + } + + const userName = userSocketMap[socket.id] || 'Unknown'; + if (!room.fileLocks) room.fileLocks = {}; + room.fileLocks[fileId] = { + socketId: socket.id, + userName: userName, + allowedUsers: {} + }; + + io.to(roomId).emit(ACTIONS.FILE_LOCKED, { fileId, socketId: socket.id, userName }); + io.to(roomId).emit(ACTIONS.LOCK_STATUS_UPDATE, { + fileLocks: room.fileLocks, + activeEditors: room.activeEditors || {} + }); + }); + + socket.on(ACTIONS.FILE_UNLOCKED, ({ roomId, fileId }) => { + if (!roomState[roomId]) return; + const room = roomState[roomId]; + const lock = room.fileLocks?.[fileId]; + if (!lock) return; + + const isRoomAdmin = room.admin === socket.id; + if (lock.socketId !== socket.id && !isRoomAdmin) { + socket.emit(ACTIONS.PERMISSION_DENIED, { message: 'You cannot unlock a file locked by someone else.' }); + return; + } + + delete room.fileLocks[fileId]; + io.to(roomId).emit(ACTIONS.FILE_UNLOCKED, { fileId }); + io.to(roomId).emit(ACTIONS.LOCK_STATUS_UPDATE, { + fileLocks: room.fileLocks, + activeEditors: room.activeEditors || {} + }); + }); + + socket.on(ACTIONS.REQUEST_EDIT_ACCESS, ({ roomId, fileId }) => { + if (!roomState[roomId]) return; + const room = roomState[roomId]; + const lock = room.fileLocks?.[fileId]; + if (!lock) return; + + const userName = userSocketMap[socket.id] || 'Unknown'; + const fileName = room.fileSystem[fileId]?.name || 'file'; + + // Route request to current lock owner + io.to(lock.socketId).emit(ACTIONS.REQUEST_EDIT_ACCESS, { + requesterSocketId: socket.id, + requesterName: userName, + fileId, + fileName + }); + }); + + socket.on(ACTIONS.APPROVE_EDIT_ACCESS, ({ roomId, fileId, requesterSocketId }) => { + if (!roomState[roomId]) return; + const room = roomState[roomId]; + const lock = room.fileLocks?.[fileId]; + if (!lock) return; + + const isRoomAdmin = room.admin === socket.id; + if (lock.socketId !== socket.id && !isRoomAdmin) return; + + if (!lock.allowedUsers) lock.allowedUsers = {}; + lock.allowedUsers[requesterSocketId] = true; + + // Notify requester + io.to(requesterSocketId).emit(ACTIONS.APPROVE_EDIT_ACCESS, { fileId }); + + // Broadcast lock status update to sync all clients + io.to(roomId).emit(ACTIONS.LOCK_STATUS_UPDATE, { + fileLocks: room.fileLocks, + activeEditors: room.activeEditors || {} + }); + }); + + socket.on(ACTIONS.REJECT_EDIT_ACCESS, ({ roomId, fileId, requesterSocketId }) => { + if (!roomState[roomId]) return; + const room = roomState[roomId]; + const lock = room.fileLocks?.[fileId]; + if (!lock) return; + + const isRoomAdmin = room.admin === socket.id; + if (lock.socketId !== socket.id && !isRoomAdmin) return; + + const fileName = room.fileSystem[fileId]?.name || 'file'; + + // Notify requester + io.to(requesterSocketId).emit(ACTIONS.REJECT_EDIT_ACCESS, { fileId, fileName }); + }); + + socket.on(ACTIONS.ACTIVE_EDITOR_CHANGED, ({ roomId, fileId }) => { + if (!roomState[roomId]) return; + const room = roomState[roomId]; + if (!room.activeEditors) room.activeEditors = {}; + + const userName = userSocketMap[socket.id] || 'Unknown'; + + if (fileId) { + room.activeEditors[fileId] = { socketId: socket.id, userName }; + } else { + // If fileId is null, user closed all files, so remove them from any file they were editing + for (const [fId, editor] of Object.entries(room.activeEditors)) { + if (editor.socketId === socket.id) { + delete room.activeEditors[fId]; + } + } + } + + io.to(roomId).emit(ACTIONS.LOCK_STATUS_UPDATE, { + fileLocks: room.fileLocks || {}, + activeEditors: room.activeEditors + }); + }); + + socket.on('disconnecting', () => { const rooms = [...socket.rooms]; rooms.forEach((roomId) => { @@ -405,6 +547,45 @@ io.on('connection', (socket) => { const clients = getAllConnectedClients(roomId).filter(c => c.socketId !== socket.id); socket.to(roomId).emit(ACTIONS.PERMISSION_CHANGED, { clients }); } + + // Clean up file locks owned by the disconnecting client + if (roomState[roomId] && roomState[roomId].fileLocks) { + let lockChanged = false; + for (const [fId, lock] of Object.entries(roomState[roomId].fileLocks)) { + if (lock.socketId === socket.id) { + delete roomState[roomId].fileLocks[fId]; + lockChanged = true; + io.to(roomId).emit(ACTIONS.FILE_UNLOCKED, { fileId: fId }); + } else if (lock.allowedUsers && lock.allowedUsers[socket.id]) { + // Remove user from allowed list + delete lock.allowedUsers[socket.id]; + lockChanged = true; + } + } + if (lockChanged) { + io.to(roomId).emit(ACTIONS.LOCK_STATUS_UPDATE, { + fileLocks: roomState[roomId].fileLocks, + activeEditors: roomState[roomId].activeEditors || {} + }); + } + } + + // Clean up active editors + if (roomState[roomId] && roomState[roomId].activeEditors) { + let editorChanged = false; + for (const [fId, editor] of Object.entries(roomState[roomId].activeEditors)) { + if (editor.socketId === socket.id) { + delete roomState[roomId].activeEditors[fId]; + editorChanged = true; + } + } + if (editorChanged) { + io.to(roomId).emit(ACTIONS.LOCK_STATUS_UPDATE, { + fileLocks: roomState[roomId].fileLocks || {}, + activeEditors: roomState[roomId].activeEditors + }); + } + } }); delete userSocketMap[socket.id]; delete socketRoomMap[socket.id]; diff --git a/src/Actions.js b/src/Actions.js index 5f49877..1714106 100644 --- a/src/Actions.js +++ b/src/Actions.js @@ -25,6 +25,14 @@ const ACTIONS = { // Group Chat CHAT_SEND: 'chat_send', CHAT_RECEIVE: 'chat_receive', + // File Lock & Edit Access Request + FILE_LOCKED: 'file_locked', + FILE_UNLOCKED: 'file_unlocked', + REQUEST_EDIT_ACCESS: 'request_edit_access', + APPROVE_EDIT_ACCESS: 'approve_edit_access', + REJECT_EDIT_ACCESS: 'reject_edit_access', + ACTIVE_EDITOR_CHANGED: 'active_editor_changed', + LOCK_STATUS_UPDATE: 'lock_status_update', }; module.exports = ACTIONS; \ No newline at end of file diff --git a/src/App.css b/src/App.css index 7f4e714..2b80134 100644 --- a/src/App.css +++ b/src/App.css @@ -2158,3 +2158,138 @@ body[data-theme='light'] .console-separator::after { 50% { transform: scale(1.1); } 100% { transform: scale(1); } } + +/* 🔒 File Lock & Edit Access Request System Styles */ +.fs-node-row { + border-left: 2.5px solid transparent; +} + +.fs-node-locked { + border-left-color: #ff5555; + background: rgba(255, 85, 85, 0.03); +} + +.fs-node-locked-by-me { + border-left-color: #4aed88; + background: rgba(74, 237, 136, 0.03); +} + +.fs-node-locked-allowed { + border-left-color: #ffd700; + background: rgba(255, 215, 0, 0.03); +} + +.file-lock-indicator { + margin-left: 6px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.file-lock-indicator.lock-me { + color: #4aed88; +} + +.file-lock-indicator.lock-allowed { + color: #ffd700; +} + +.file-lock-indicator.lock-other { + color: #ff5555; +} + +.tab-lock-indicator { + color: #ff5555; + display: inline-flex; + align-items: center; +} + +.currently-editing-presence { + font-size: 0.75rem; + font-weight: 600; + color: #8be9fd; + background: rgba(139, 233, 253, 0.1); + padding: 2px 8px; + border-radius: 4px; + margin-left: 12px; + display: inline-flex; + align-items: center; + gap: 4px; + border: 1px solid rgba(139, 233, 253, 0.2); +} + +.editor-lock-btn { + background: rgba(255, 255, 255, 0.06); + border: 1px solid var(--border-color); + color: var(--text-color); + font-size: 0.72rem; + font-weight: 600; + padding: 3px 8px; + border-radius: 4px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 4px; + transition: all 0.2s ease; +} + +.editor-lock-btn:hover { + background: rgba(255, 255, 255, 0.12); +} + +.lock-action-btn:hover { + border-color: #8be9fd; + color: #8be9fd; + background: rgba(139, 233, 253, 0.05); +} + +.unlock-action-btn { + border-color: #ff5555; + color: #ff5555; + background: rgba(255, 85, 85, 0.05); +} + +.unlock-action-btn:hover { + background: #ff5555; + color: #fff; +} + +.request-action-btn { + border-color: #ffd700; + color: #ffd700; + background: rgba(255, 215, 0, 0.05); +} + +.request-action-btn:hover { + background: #ffd700; + color: #1c1e29; +} + +.readonly-badge.lock-banner { + background: #ff5555; + color: #fff; + display: flex; + align-items: center; + gap: 12px; + pointer-events: auto; + padding: 8px 16px; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); +} + +.lock-banner-request-btn { + background: #fff; + color: #ff5555; + border: none; + font-size: 0.78rem; + font-weight: 700; + padding: 4px 10px; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s, color 0.2s; +} + +.lock-banner-request-btn:hover { + background: #f1f1f1; +} + diff --git a/src/components/Editor.js b/src/components/Editor.js index 24c6116..978a3e4 100644 --- a/src/components/Editor.js +++ b/src/components/Editor.js @@ -40,7 +40,25 @@ function getColor(clientId) { return cursorColors[Math.abs(clientId) % cursorColors.length]; } -const Editor = ({ socketRef, roomId, fileId, fileName, onCodeChange, userName, canWrite, editorTheme, onEditorReady, initialContent }) => { +const Editor = ({ + socketRef, + roomId, + fileId, + fileName, + onCodeChange, + userName, + canWrite, + editorTheme, + onEditorReady, + initialContent, + fileLocks = {}, + activeEditors = {}, + onLockFile, + onUnlockFile, + onRequestEditAccess, + isAdmin = false, + roomCanWrite = false +}) => { const editorRef = useRef(null); const textareaRef = useRef(null); const providerRef = useRef(null); @@ -217,7 +235,76 @@ const Editor = ({ socketRef, roomId, fileId, fileName, onCodeChange, userName, c {peer.name[0]?.toUpperCase()} ))} - {fileName} + + {(() => { + const fileLock = fileLocks?.[fileId]; + const isLocked = !!fileLock; + const isLockedByMe = isLocked && fileLock.socketId === socketRef.current?.id; + const isAllowedEditor = isLocked && fileLock.allowedUsers?.[socketRef.current?.id]; + const activeEditor = activeEditors?.[fileId]; + return ( + <> + {activeEditor && activeEditor.socketId !== socketRef.current?.id && ( + + ✏️ Currently editing: {activeEditor.userName} + + )} + + + {fileName} + {isLocked && ( + + (🔒 Locked by {fileLock.userName}{isLockedByMe ? ' - You' : ''}) + + )} + + + {roomId && fileId && socketRef.current && ( +
+ {!isLocked ? ( + roomCanWrite && ( + + ) + ) : ( + (isLockedByMe || isAdmin) ? ( + + ) : ( + !isAllowedEditor && roomCanWrite && ( + + ) + ) + )} +
+ )} + + ); + })()} + {modeLabel} diff --git a/src/components/FileExplorer.js b/src/components/FileExplorer.js index bce35f8..748321b 100644 --- a/src/components/FileExplorer.js +++ b/src/components/FileExplorer.js @@ -11,6 +11,8 @@ import { Trash2, ChevronRight, ChevronDown, + Lock, + Unlock, } from 'lucide-react'; // File icon by extension — returns a Lucide component @@ -23,7 +25,7 @@ function FileIcon({ name }) { } // Single tree node -function FileNode({ node, fileSystem, depth, activeFileId, onFileClick, onCreateNode, onDeleteNode, onRenameNode, canWrite }) { +function FileNode({ node, fileSystem, depth, activeFileId, onFileClick, onCreateNode, onDeleteNode, onRenameNode, canWrite, fileLocks = {}, socketId = null }) { const [expanded, setExpanded] = useState(true); const [renaming, setRenaming] = useState(false); const [renameVal, setRenameVal] = useState(node.name); @@ -35,6 +37,10 @@ function FileNode({ node, fileSystem, depth, activeFileId, onFileClick, onCreate const isFolder = node.type === 'folder'; const isRoot = node.id === 'root'; const isActive = activeFileId === node.id; + const fileLock = fileLocks?.[node.id]; + const isLocked = !isFolder && !!fileLock; + const isLockedByMe = isLocked && fileLock.socketId === socketId; + const isAllowedEditor = isLocked && fileLock.allowedUsers && fileLock.allowedUsers[socketId]; const handleRenameSubmit = () => { const trimmed = renameVal.trim(); @@ -65,8 +71,9 @@ function FileNode({ node, fileSystem, depth, activeFileId, onFileClick, onCreate return (
{ if (isFolder) setExpanded(e => !e); else onFileClick(node.id); @@ -96,7 +103,17 @@ function FileNode({ node, fileSystem, depth, activeFileId, onFileClick, onCreate onClick={e => e.stopPropagation()} /> ) : ( - {node.name} + + {node.name} + {isLocked && ( + + + + )} + )} {canWrite && ( e.stopPropagation()}> @@ -159,6 +176,8 @@ function FileNode({ node, fileSystem, depth, activeFileId, onFileClick, onCreate onDeleteNode={onDeleteNode} onRenameNode={onRenameNode} canWrite={canWrite} + fileLocks={fileLocks} + socketId={socketId} /> ))}
@@ -166,7 +185,7 @@ function FileNode({ node, fileSystem, depth, activeFileId, onFileClick, onCreate } // Main FileExplorer -const FileExplorer = ({ fileSystem, activeFileId, onFileClick, onCreateNode, onDeleteNode, onRenameNode, canWrite }) => { +const FileExplorer = ({ fileSystem, activeFileId, onFileClick, onCreateNode, onDeleteNode, onRenameNode, canWrite, fileLocks = {}, socketId = null }) => { const root = fileSystem['root']; if (!root) return null; @@ -186,6 +205,8 @@ const FileExplorer = ({ fileSystem, activeFileId, onFileClick, onCreateNode, onD onDeleteNode={onDeleteNode} onRenameNode={onRenameNode} canWrite={canWrite} + fileLocks={fileLocks} + socketId={socketId} />
diff --git a/src/components/FileTabs.js b/src/components/FileTabs.js index d7dc9d7..27e010e 100644 --- a/src/components/FileTabs.js +++ b/src/components/FileTabs.js @@ -1,5 +1,5 @@ import React from 'react'; -import { FileCode2, File } from 'lucide-react'; +import { FileCode2, File, Lock } from 'lucide-react'; function FileIcon({ name = '' }) { const ext = name.split('.').pop().toLowerCase(); @@ -8,7 +8,7 @@ function FileIcon({ name = '' }) { return ; } -const FileTabs = ({ openFiles, fileSystem, activeFileId, onTabClick, onTabClose }) => { +const FileTabs = ({ openFiles, fileSystem, activeFileId, onTabClick, onTabClose, fileLocks = {} }) => { if (openFiles.length === 0) return null; return ( @@ -24,7 +24,14 @@ const FileTabs = ({ openFiles, fileSystem, activeFileId, onTabClick, onTabClose onClick={() => onTabClick(fileId)} > - {node.name} + + {node.name} + {fileLocks?.[fileId] && ( + + + + )} +